From 1a07dad17c0d2bec0e7723b87adfc7eb5b4cc718 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 6 Aug 2026 11:54:44 +0330 Subject: [PATCH] feat(online-booking): add design document for online resource booking with deposit feature --- docs/new_feture/online-resource-booking.md | 607 +++++++++++++++++++++ 1 file changed, 607 insertions(+) create mode 100644 docs/new_feture/online-resource-booking.md diff --git a/docs/new_feture/online-resource-booking.md b/docs/new_feture/online-resource-booking.md new file mode 100644 index 00000000..7c5eda73 --- /dev/null +++ b/docs/new_feture/online-resource-booking.md @@ -0,0 +1,607 @@ +# نوبت‌دهی منبع‌محورِ آنلاین با بیعانهٔ درصدی + +> **دامنه:** `clinicpro` (بک‌اند + پنل) و `nobat724_front` (سایت عمومی) +> **وضعیت:** سند طراحی. کد نوشته نشده. +> **تاریخ:** ۱۴۰۵ / 2026-08 + +این سند حاصل یک جلسهٔ تصمیم‌گیری است. هر بند «چه» را می‌گوید و «چرا» را کنارش. +تصمیم‌های گرفته‌شده در بخش ۳ فهرست‌اند و بقیهٔ سند نتیجهٔ آن‌هاست. + +--- + +## ۱. مسئله + +نوبت‌دهی منبع‌محور در `clinicpro` ساخته شده و کار می‌کند، ولی فقط داخل پنل. +هیچ بیماری از `nobat724` نمی‌تواند روی یک دستگاه یا اتاق نوبت بگیرد. + +خواسته سه تکه دارد: + +۱. مدیر کلینیک بتواند یک منبع را «آنلاین» کند تا در سایت عمومی دیده شود. +۲. برای هر سرویس، درصدی از هزینه به‌عنوان بیعانه آنلاین گرفته شود. +۳. مشخص شود این نوبت‌دهی در پروفایل پزشک است یا در کلینیک. + +--- + +## ۲. آنچه امروز واقعاً هست + +این بخش از روی کد نوشته شده، نه از روی مستندات. تفاوتشان در ۲.۴ آمده. + +### ۲.۱ موتور منبع‌محور — کامل، ولی خصوصی + +| موجودیت | مسیر | +|---|---| +| `ClinicResource` | `src/Resource/Entity/ClinicResource.php` | +| `ResourceServiceOffering` | `src/Resource/Entity/ResourceServiceOffering.php` | +| `ResourceCalendar` / `ResourceException` | `src/Resource/Entity/` | +| `AppointmentHold` / `OccupancyBucket` | `src/Appointment/Booking/Entity/` | +| `ResourceOccupancy` | `src/Appointment/Availability/Entity/` | + +اندپوینت‌های موجود و فعال: + +``` +POST /api/v1/appointment-availability جستجوی وقت روی همهٔ منابع + پیشنهاد assignment +POST /api/v1/appointment-hold رزرو موقت ۹۰۰ ثانیه‌ای +POST /api/v1/appointment-confirm ثبت نهایی +GET /api/v1/resource/{uuid}/service-slots اسلات per منبع +POST /api/v1/pricing/quote پیش‌نمایش فاکتور +``` + +همه پشت JWT‌اند و محیط را از `EntityContextResolver` می‌گیرند. +بیمار ناشناس محیط ندارد، پس هیچ‌کدام از سایت عمومی قابل استفاده نیستند. + +سه واقعیت که طراحی را قفل می‌کنند: + +- **منبع مال یک محیط است.** محیط یعنی جفت `(entity_type, entity_id)` که یا مطب شخصی + یک پزشک است یا یک کلینیک. پس منبع از قبل هر دو حالت را می‌پذیرد. +- **هر منبع پزشک ناظر دارد** (`supervisor`، در ساخت الزامی). نوبتِ منبع پزشکش را از + همین‌جا می‌گیرد. +- **تضمین ضدتداخل در دیتابیس است**، با کلید یکتای زیر روی سطل‌های پنج‌دقیقه‌ای: + +```sql +UNIQUE (resource_id, bucket_at, seat) +``` + +### ۲.۲ پرداخت — یک عدد ثابت + +مبلغ هر پرداخت نوبت از کلید `appointment_fee_rials` تنظیمات سایت خوانده می‌شود. +نه از سرویس می‌آید، نه از فاکتور، نه از client. + +پول به ترمینال **پلتفرم** می‌رود، نه به حساب کلینیک. +بعد از آن دفتر کیف پول روی `user_id` و درخواست تسویه با تأیید ادمین. + +کیف پول جدول موجودی ندارد؛ `wallet_transactions` یک دفتر است و موجودی از جمع ردیف‌ها +درمی‌آید. بیمار هم `User` است، پس اعتبار دادن به بیمار هیچ مهاجرت ساختاری نمی‌خواهد. + +### ۲.۳ بیعانه — سه تکهٔ ناهم‌راستا + +- `Appointment.deposit_required` و `deposit_amount_rials` — عدد دستی که کاربر پنل وارد + می‌کند. هیچ ربطی به قیمت سرویس ندارد. +- `deposit_percent` — فقط پارامتر ورودی `POST /pricing/quote`. هیچ‌جا ذخیره نمی‌شود. + یعنی هر client هر عددی بفرستد همان محاسبه می‌شود. +- `ResourceServiceOffering.price_rials` — یک override قیمت per منبع که در کد هست ولی + `docs/api/pricing.md` می‌گوید «تنها منبع قیمت `ServiceItem.price_rials` است». + +هیچ‌کدام قابل استفاده در پرداخت آنلاین نیستند. + +### ۲.۴ ⚠️ سه مستندی که کد ندارند + +بررسی مستقیم `src/` نشان می‌دهد این‌ها **پیاده‌سازی نشده‌اند**، هرچند مستند دارند: + +| مستند | ادعا | واقعیت در کد | +|---|---|---| +| `docs/api/cancellation.md` | سیاست لغو، جریمه، `deposit_refundable` | رشتهٔ `cancellation` در کل `src/` صفر تطابق دارد | +| `docs/api/policy.md` | موتور قوانین شش‌دسته‌ای | `PolicyController` و `policy-schema` وجود ندارند | +| `appointment/{uuid}/cancel` و `cancellation-preview` | فراخوانی‌شده در `nobat724_front` | route وجود ندارد؛ لغو با `PATCH /appointment/{uuid}/status` انجام می‌شود | + +این روی بند ۵ همین سند اثر مستقیم دارد: مبلغ قابل بازگشت قرار است از سیاست لغو بیاید، +و آن سیاست هنوز ساخته نشده. یا باید در همین فاز ساخته شود، یا فاز اول با یک قاعدهٔ +ثابت شروع کند. تصمیمش در ۹.۱ آمده. + +### ۲.۵ شکاف اشغال — بلوکرِ اصلی + +دو مسیر ثبت نوبت روی منبع وجود دارد و فقط یکی جدول اشغال را پر می‌کند: + +``` +POST /api/v1/my/appointment با resource_uuid → resource_occupancy نمی‌سازد +موتور منبع‌محور (hold/confirm) → resource_occupancy می‌سازد +``` + +موتور منبع‌محور فقط `resource_occupancy` را می‌خواند. +پس نوبتی که منشی از پنل روی لیزر ثبت کرده، برای آن موتور **نامرئی** است. + +تا امروز بی‌خطر بوده چون آن موتور فقط داخل پنل بود و پنل هر دو منبع را می‌خواند. +با عمومی‌شدن، دابل‌بوکینگ قطعی است — و بدتر، قفل دیتابیس هم نمی‌گیردش، چون آن کلید +یکتا فقط روی `resource_occupancy` است. + +### ۲.۶ سایت عمومی — پزشک‌محورِ خالص + +مسیر نوبت‌گیری `/appointment/[doctorId]` است و همهٔ اندپوینت‌ها `doctor_uuid` می‌گیرند: + +``` +GET api/v1/appointment-booking-locations/{doctor_uuid} +GET api/v1/appointment-booking-services/{doctor_uuid} +GET api/v1/appointment-slots?doctor_uuid=… +GET api/v1/appointment-service-slots?doctor_uuid=… +``` + +`clinic_uuid` فقط یک پارامتر کنار آن‌هاست که «کدام برنامهٔ هفتگی» را انتخاب می‌کند. +نبودنش یعنی مطب شخصی، نه wildcard. + +صفحهٔ `/clinic/[slug]` ویترین است: اطلاعات کلینیک و فهرست پزشکان. نوبت‌دهی مستقل ندارد. + +`booking_mode` در `WeeklySchedule.setting.meta` می‌نشیند و امروز دو مقدار دارد: +`slot` و `service`. + +--- + +## ۳. تصمیم‌ها + +| # | تصمیم | چرا | +|---|---|---| +| ۱ | بیمار فقط **سرویس و زمان** انتخاب می‌کند | تخصیص منبع را موتور می‌دهد؛ نمایشش به بیمار یعنی تصمیمی که هیچ بیماری نمی‌تواند بگیرد | +| ۲ | نقطهٔ ورود را **مالکِ منبع** تعیین می‌کند | منبع و سرویس هر دو مال یک محیط‌اند؛ URL نباید دربارهٔ مالکیت تقویم دروغ بگوید | +| ۳ | آنلاین‌بودن = **پرچم روی منبع × `bookable` روی سرویس** | «این دستگاه قابل عرضه است» و «این خدمت قابل فروش است» دو سؤال جدایند | +| ۴ | درصد بیعانه روی **`ServiceItem`**، با پیش‌فرض محیط | قیمت روی سرویس است، پس درصدِ قیمت هم همان‌جاست | +| ۵ | درصد روی **قیمت پایه** اعمال می‌شود | بیمهٔ بیمار آنلاین معلوم نیست؛ عدد باید برای همه یکسان و قابل پیش‌بینی باشد | +| ۶ | بیعانه **اختیاری**؛ نبودش یعنی همان `appointment_fee_rials` | یک تراکنش، دو منبع مبلغ | +| ۷ | پرداخت **بعد از ثبت نهایی**، روی نوبتِ پرداخت‌نشده | کل ماشین پرداخت و استرداد دست‌نخورده می‌ماند | +| ۸ | نام پزشک **پیش از پرداخت** نشان داده می‌شود | بیمار قبل از پول دادن بداند نزد کی می‌رود | +| ۹ | استرداد به **کیف پول بیمار**؛ نقد فقط دستی | فوری و بدون درگاه؛ نقدشدن یک تصمیم انسانی می‌ماند | +| ۱۰ | مسیر پنل هم **`resource_occupancy` بنویسد** | یک منبع حقیقت برای اشغال؛ بدون آن قفل دیتابیس بی‌اثر است | +| ۱۱ | **سه اندپوینت عمومی تازه**، محیط از URL | قرارداد متفاوت است نه فقط مجوز متفاوت | +| ۱۲ | در سایت، **مقدار سوم `booking_mode`** روی همان صفحه | لاگین و پرداخت و نتیجه و داشبورد همه مشترک‌اند | +| ۱۳ | تنظیم‌کنندهٔ درصد = **مالک محیط** | درصد یک فیلد کنار قیمت است؛ دو مالک برای دو فیلد هم‌ردیف یعنی دو مدل دسترسی در یک فرم | + +فرض‌های پذیرفته‌شده: + +- رزرو موقت هم لاگین می‌خواهد. وگرنه ناشناس می‌تواند صندلی‌ها را خالی نگه دارد. +- بیعانه فقط برای رزرو منبع‌محور است. مسیر `slot` و `service` فعلی دست‌نخورده می‌ماند. + +--- + +## ۴. واژگان + +| واژه | معنی دقیق | +|---|---| +| **منبع** | هرچیزی که ممکن است اشغال باشد: پزشک، پرسنل، اتاق، دستگاه، تخت | +| **منبع آنلاین** | منبعی که پرچم `online_bookable` دارد و در سایت عمومی دیده می‌شود | +| **سرویسِ آنلاین** | سرویسی که هم `bookable` است و هم دست‌کم یک offering فعال روی یک منبع آنلاین دارد | +| **ناظر** | پزشکِ مسئول یک منبع. پزشکِ نوبت از او می‌آید. با «پلِ منبع» فرق دارد | +| **بیعانه** | پیش‌پرداختِ بخشی از هزینهٔ سرویس. از فاکتور نهایی کسر می‌شود، اضافه بر آن نیست | +| **کارمزد نوبت** | همان `appointment_fee_rials` سراسری. درآمد پلتفرم، نه پیش‌پرداخت خدمت | +| **نوبتِ پرداخت‌نشده** | نوبتی که ثبت شده ولی مهلت دارد؛ با موفقیت پرداخت مهلتش برداشته می‌شود | +| **اعتبار بیمار** | موجودی کیف پول بیمار. خرج رزرو بعدی می‌شود | + +واژه‌هایی که عمداً استفاده **نمی‌شوند**: + +- «شعبه» — از محصول حذف شده؛ لنگر محیطی همان `doctor_addresses` است. +- «تخفیف» به‌جای بیعانه — بیعانه مبلغ را کم نمی‌کند، فقط زمان پرداختش را جلو می‌اندازد. + +--- + +## ۵. تغییرات دیتابیس + +### ۵.۱ `clinic_resources` + +```sql +ALTER TABLE clinic_resources + ADD COLUMN online_bookable TINYINT(1) NOT NULL DEFAULT 0; +``` + +پیش‌فرض `0` عمدی است. دستگاه داخلی و اتاق عمل نباید با یک deploy به سایت نشت کنند. + +### ۵.۲ `service_items` + +```sql +ALTER TABLE service_items + ADD COLUMN online_deposit_percent SMALLINT NULL; +``` + +`NULL` یعنی «از پیش‌فرض محیط بخوان»، نه «صفر». +تفاوتشان لازم است: صفر یعنی «این سرویس عمداً بیعانه ندارد». + +بازهٔ مجاز `0..100`. بیرونش `422`. + +### ۵.۳ تنظیمات محیط + +یک کلید تازه در تنظیمات همان محیط: + +``` +online_deposit_percent_default +``` + +نبودنش یعنی صفر، یعنی رفتار فعلی. + +### ۵.۴ `appointments` + +فیلد تازه‌ای لازم نیست. `deposit_required` و `deposit_amount_rials` از قبل هستند و +همان‌ها با مقدارِ محاسبه‌شدهٔ سرور پر می‌شوند. + +⚠️ تنها تفاوت: تا امروز این دو را کاربر پنل دستی می‌نوشت. از این پس در مسیر آنلاین +**سرور** می‌نویسد و ورودی client پذیرفته نمی‌شود. + +### ۵.۵ `wallet_transactions` + +ساختار تغییر نمی‌کند. فقط نوع تازه‌ای از `description` و یک `payment_id` که به پرداختِ +اصلی اشاره می‌کند تا زنجیرهٔ «پرداخت ← لغو ← اعتبار ← مصرف» قابل ردیابی بماند. + +### ۵.۶ مهاجرت backfill اشغال + +برای هر نوبتِ آیندهٔ دارای `resource_id` که ردیف `resource_occupancy` ندارد، ردیف +ساخته شود. نوبت‌های گذشته لازم نیستند — اشغالِ گذشته کسی را بلاک نمی‌کند. + +⚠️ این مهاجرت باید **قبل از** روشن‌کردن هر منبع آنلاین اجرا شود. + +--- + +## ۶. قرارداد API + +### ۶.۱ اندپوینت‌های عمومی تازه + +هر سه بدون JWT. محیط از مسیر می‌آید، نه از توکن. + +``` +GET /api/v1/public/booking/{entityType}/{entityUuid}/services +POST /api/v1/public/booking/{entityType}/{entityUuid}/availability +POST /api/v1/public/booking/{entityType}/{entityUuid}/hold +``` + +`entityType` یکی از `doctor` یا `clinic` است. + +درونشان دقیقاً همان سرویس‌های موجود صدا زده می‌شوند — +`AvailabilityService`, `HoldService`, `ResourceServiceResolver`. +منطق تازه‌ای نوشته نمی‌شود؛ فقط منبع محیط عوض می‌شود. + +#### `GET …/services` + +فقط سرویس‌هایی که هر سه شرط را دارند: + +- `service_items.bookable = 1` +- دست‌کم یک `ResourceServiceOffering` فعال دارند +- آن offering روی منبعی با `online_bookable = 1` و `active = 1` است + +پاسخ: + +```json +{ + "success": true, + "data": { + "entity": { "type": "clinic", "uuid": "…", "name": "کلینیک پوست یزد" }, + "sections": [ + { + "uuid": "…", "name": "لیزر", + "items": [ + { + "uuid": "…", + "name": "لیزر فول‌بادی", + "price_rials": 20000000, + "deposit_percent": 30, + "deposit_rials": 6000000, + "duration_minutes": 50 + } + ] + } + ] + } +} +``` + +`deposit_rials` را **سرور** حساب می‌کند. client هرگز مبلغ نمی‌فرستد. + +`duration_minutes` از زنجیرهٔ حلِ منبع می‌آید، نه از `duration_minutes` خام سرویس — +همان «RF فرکشنال» روی یک دستگاه ۵۰ دقیقه است و روی دیگری ۴۰. وقتی چند منبع آن سرویس +را می‌دهند، کمینهٔ مدت‌ها نمایش داده می‌شود و مدت واقعی در پاسخ رزرو موقت می‌آید. + +#### `POST …/availability` + +بدنه: + +```json +{ "service_uuid": "…", "from": 1785529800, "to": 1785616200, "item_uuids": [] } +``` + +همان قواعد اندپوینت خصوصی: هر دو سر بازه شامل، سقف ۹۰ روز. +تفاوت‌ها: + +- فقط منابع `online_bookable` در جستجو شرکت می‌کنند. +- `assignment` در پاسخ **برنمی‌گردد**. بیمار منبع را نمی‌بیند. +- به‌جایش هر اسلات `doctor` دارد: `{ uuid, name }` از ناظرِ منبعِ پیشنهادی. + +```json +{ + "success": true, + "data": { + "slots": [ + { "start": 1785562200, "end": 1785565200, + "doctor": { "uuid": "…", "name": "امیر کاظمی" } } + ] + } +} +``` + +#### `POST …/hold` + +**لاگین لازم است.** تنها اندپوینت از این سه که JWT می‌خواهد. +دلیل: رزرو موقت صندلی می‌گیرد، و ناشناسی که صندلی می‌گیرد قابل پاسخگویی نیست. + +پاسخ همان `hold_uuid` و بازه و `expires_at` است، به‌علاوهٔ: + +```json +{ + "doctor": { "uuid": "…", "name": "امیر کاظمی" }, + "deposit_rials": 6000000, + "price_rials": 20000000 +} +``` + +این پاسخ ورودی صفحهٔ تأیید است. بیمار پیش از پرداخت هر سه را می‌بیند: +پزشک، هزینهٔ کل، مبلغی که الان می‌پردازد. + +### ۶.۲ تغییر در `POST /api/v1/appointment-confirm` + +نوبت با `deposit_required` و `deposit_amount_rials`ِ محاسبه‌شده ساخته می‌شود و +`expires_at` می‌گیرد — دقیقاً مثل نوبت‌های عمومی امروز. + +`doctor_uuid` در بدنه دیگر لازم نیست وقتی رزرو از مسیر عمومی آمده؛ از ناظرِ منبع حل +می‌شود. فرستادن پزشکی غیر از ناظر ⇒ `422`. + +### ۶.۳ تغییر در `POST /api/v1/payment/appointment` + +امروز مبلغ همیشه `appointment_fee_rials` است. از این پس: + +``` +اگر appointment.deposit_required → مبلغ = deposit_amount_rials +در غیر این صورت → مبلغ = appointment_fee_rials +``` + +انتخاب همچنان **سمت سرور** انجام می‌شود و از `appointment_uuid` مشتق است. +هیچ فیلد تازه‌ای در بدنهٔ درخواست اضافه نمی‌شود. + +اگر بیمار موجودی کیف پول داشته باشد، اول از آن کسر می‌شود و فقط مابقی به درگاه می‌رود. +صفر شدن مابقی یعنی پرداخت بدون درگاه: نوبت مستقیم تأیید می‌شود و ردیف `Payment` با +`gateway = wallet` ثبت می‌گردد تا گزارش مالی ردیف کم نداشته باشد. + +### ۶.۴ تغییر در `GET /api/v1/payment/config` + +یک فیلد تازه که فرانت با آن می‌فهمد کدام عدد را نشان بدهد: + +```json +{ "appointment_fee_rials": 150000, "wallet_balance_rials": 0 } +``` + +### ۶.۵ اندپوینت‌های پنل که تغییر می‌کنند + +``` +PATCH /api/v1/resource/{uuid} + online_bookable +PATCH /api/v1/service-item/{uuid} + online_deposit_percent +``` + +هر دو با همان مجوزهای فعلی. `online_bookable` با `appointment_settings.update`، +`online_deposit_percent` با `services.update`. + +⚠️ `PUT /api/v1/resource/{uuid}/services` که offeringها را می‌نویسد بدون تغییر می‌ماند. + +--- + +## ۷. جریان کامل + +``` +بیمار در صفحهٔ کلینیک یا پزشک + │ GET …/services فهرست سرویس‌ها + قیمت + بیعانه + ▼ +انتخاب سرویس + │ POST …/availability ساعت‌های آزاد + نام پزشک هر ساعت + ▼ +انتخاب ساعت → لاگین اگر نکرده + │ POST …/hold صندلی گرفته شد، ۹۰۰ ثانیه مهلت + ▼ +صفحهٔ تأیید: پزشک ‧ هزینهٔ کل ‧ مبلغ پرداخت الان + │ POST /appointment-confirm نوبتِ پرداخت‌نشده با مهلت + ▼ + │ POST /payment/appointment مبلغ = بیعانه یا کارمزد + ▼ +درگاه → callback + │ مهلت برداشته می‌شود، پیامک می‌رود + ▼ +نوبت pending → تأیید کلینیک → حضور → تسویهٔ مابقی در محل +``` + +نکته‌های ترتیب: + +- **مهلت رزرو موقت و مهلت پرداخت یک عددند** (۹۰۰ ثانیه). دو عدد متفاوت یعنی دو حقیقت + متفاوت و یکی از آن دو همیشه غلط است. +- **پس از `confirm`، رزرو موقت هیچ منبعی را دوباره نمی‌گیرد.** صندلی از لحظهٔ رزرو گرفته + شده و اینجا فقط برچسبش عوض می‌شود. +- **پرداخت نوبت را قطعی نمی‌کند.** نوبت `pending` می‌ماند و قطعی‌شدن همچنان تصمیم + کلینیک است — رفتار فعلی، بدون تغییر. +- **انقضای بدون پرداخت** باید هم نوبت را `expired` کند و هم سطل‌های یکتای اشغال را حذف. + ردیف‌های `resource_occupancy` حذف فیزیکی نمی‌شوند و به `released` می‌روند؛ حذف نکردنِ + سطل‌ها یعنی آن زمان برای همیشه قفل می‌ماند. + +--- + +## ۸. پیش‌نیاز اجباری — یکی‌شدن جدول اشغال + +بدون این بند، بقیهٔ سند قابل اجرا نیست. + +**قاعده:** هر نوبتی که `resource_id` دارد، از هر مسیری که ساخته شود، ردیف +`resource_occupancy` می‌سازد. + +مسیرهای درگیر: + +``` +POST /api/v1/my/appointment با resource_uuid ← امروز نمی‌سازد +POST /api/v1/appointment مسیر عمومی فعلی ← امروز نمی‌سازد +مسیر ادمین در src/Admin/ ← بررسی شود +``` + +جای درست این کار **یک سرویس مشترک** است، نه تکرار در هر controller. +هر جا نوبت با منبع ساخته یا لغو یا جابه‌جا می‌شود، همان سرویس صدا زده شود. + +پس از آن، خواندنِ اشغال از دو منبع در پنل دیگر لازم نیست و می‌تواند ساده شود — ولی +حذفش در همین فاز اجباری نیست و بهتر است یک فاز عقب‌تر انجام شود. + +تست مرجع: دو رزرو هم‌زمان روی یک منبع و یک بازه، یکی از دو مسیر متفاوت، باید `409` +بگیرد. اگر این تست از مسیر پنل رد شد، بند ۸ کامل نشده است. + +--- + +## ۹. لغو، استرداد، اعتبار + +### ۹.۱ ⚠️ پیش‌نیاز غایب + +سیاست لغو در `docs/api/cancellation.md` مستند شده ولی در کد وجود ندارد. +همچنین `appointment/{uuid}/cancel` و `cancellation-preview` که سایت صدایشان می‌زند. + +دو راه: + +- **الف — فاز اول با قاعدهٔ ثابت.** بیعانه تا `free_window_hours` ساعت قبل کاملاً + برمی‌گردد، بعد از آن هیچ. عدد در تنظیمات محیط، بدون موتور سیاست. +- **ب — ساخت کامل دامنهٔ لغو در همین فاز.** + +توصیه: الف. دامنهٔ لغو مسئلهٔ مستقلی است و گره‌زدنش به این قابلیت هر دو را عقب می‌اندازد. +قرارداد اعتبار طوری نوشته شود که بعداً منبع عدد از تنظیمات به سیاست منتقل شود بدون +تغییر در مسیر پول. + +### ۹.۲ جریان لغو + +``` +بیمار لغو می‌کند + │ مبلغ قابل بازگشت حساب می‌شود + ▼ +اسلات و منابع فوراً آزاد می‌شوند + │ ردیف credit در wallet_transactions + ▼ +اعتبار بیمار +``` + +اسلات **قبل از** هر عملیات مالی آزاد می‌شود. برعکسش یعنی صندلی تا تسویهٔ حساب قفل بماند. + +### ۹.۳ اعتبار بیمار + +- خرج رزرو بعدی می‌شود، خودکار، در گام پرداخت. +- برای نقد کردن، بیمار درخواست می‌دهد و ادمین با همان مسیر refund درگاه اجرا می‌کند. +- اعتبار منقضی نمی‌شود. انقضای پول مسئلهٔ حقوقی است، نه فنی. + +⚠️ استرداد ملت در sandbox شبیه‌سازی نمی‌شود (`bpRefundRequest` همیشه کد ۳۴). +یعنی مسیر نقدکردن فقط در prod قابل تست است. مسیر اعتبار کاملاً قابل تست است — یک دلیل +دیگر برای اینکه مسیر پیش‌فرض اعتبار باشد نه نقد. + +--- + +## ۱۰. تغییرات پنل `clinicpro` + +| صفحه | تغییر | +|---|---| +| `/admin/resources/{uuid}` | سوییچ «نمایش در نوبت‌دهی آنلاین». پیش‌فرض خاموش | +| فرم سرویس | فیلد «درصد بیعانهٔ آنلاین»، خالی = پیش‌فرض محیط | +| تنظیمات محیط | «درصد بیعانهٔ پیش‌فرض» | +| `/admin/resources` | ستون یا نشانِ «آنلاین» در فهرست | +| صفحهٔ نوبت | نمایش مبلغ پرداخت‌شده و مانده | + +قواعد الزامی پنل: + +- هر boolean با `components/ui/Switch` است. checkbox خام ممنوع. +- هر انتخاب با `SearchableSelect` است. `