feat(online-booking): add design document for online resource booking with deposit feature
This commit is contained in:
@@ -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` است. `<select>` بومی ممنوع.
|
||||
- صفحهٔ زیرمجموعه دکمهٔ بازگشت دارد؛ با `backTo` روی `PageHeader` یا `BackButton`.
|
||||
- هیچ طراحی تازهای ساخته نمیشود. توکنها و کامپوننتهای موجود.
|
||||
|
||||
یک هشدار در UI لازم است: وقتی مدیر منبعی را آنلاین میکند و آن منبع سرویسِ `bookable`
|
||||
ندارد، چیزی در سایت ظاهر نمیشود. سکوت در این حالت شبیه باگ است.
|
||||
|
||||
---
|
||||
|
||||
## ۱۱. تغییرات `nobat724_front`
|
||||
|
||||
### ۱۱.۱ مقدار سوم `booking_mode`
|
||||
|
||||
`resource` کنار `slot` و `service`.
|
||||
همان صفحهٔ `/appointment/[doctorId]` این حالت را هم میشناسد.
|
||||
|
||||
### ۱۱.۲ مسیر تازه برای کلینیک
|
||||
|
||||
```
|
||||
/clinic/[slug]/appointment
|
||||
```
|
||||
|
||||
همان کامپوننت نوبتگیری، با محیطِ کلینیک بهجای پزشک.
|
||||
یک کامپوننت، دو میزبان.
|
||||
|
||||
صفحهٔ `/clinic/[slug]` یک دکمهٔ «رزرو نوبت» میگیرد که فقط وقتی کلینیک منبع آنلاین
|
||||
دارد نمایش داده میشود.
|
||||
|
||||
### ۱۱.۳ گامهای تازهٔ UI
|
||||
|
||||
- کارت سرویس با دو عدد: هزینهٔ کل، و «پرداخت آنلاین: X ریال».
|
||||
- صفحهٔ تأیید پیش از پرداخت با نام پزشک.
|
||||
- در صفحهٔ پرداخت، اگر اعتبار دارد: «از اعتبار شما کسر میشود».
|
||||
|
||||
### ۱۱.۴ داشبورد بیمار
|
||||
|
||||
- تب اعتبار: موجودی و تاریخچه.
|
||||
- در فهرست نوبتها: مبلغ پرداختشده و مانده.
|
||||
|
||||
همهٔ رشتهها فارسی، تاریخها شمسی، جهت RTL.
|
||||
|
||||
---
|
||||
|
||||
## ۱۲. آنچه در دامنه نیست
|
||||
|
||||
صریح میآید تا بعداً بهعنوان «فراموش شد» برنگردد:
|
||||
|
||||
- بیمار منبع را انتخاب نمیکند و اسمش را نمیبیند.
|
||||
- پرداخت مستقیم به حساب کلینیک انجام نمیشود. پول به پلتفرم میرود و تسویه با مسیر
|
||||
موجود است.
|
||||
- تسویهٔ مابقی هزینه آنلاین نیست. حضوری است.
|
||||
- بیمهٔ بیمار در محاسبهٔ بیعانه دخالت نمیکند.
|
||||
- برداشت نقدی خودکار از کیف پول بیمار ساخته نمیشود.
|
||||
- موتور قوانین و دامنهٔ لغو در این فاز ساخته نمیشوند.
|
||||
- `booking_mode` قبلی تغییر نمیکند و مسیرهای `slot` و `service` دستنخوردهاند.
|
||||
|
||||
---
|
||||
|
||||
## ۱۳. ریسکها و مرزها
|
||||
|
||||
| ریسک | مهار |
|
||||
|---|---|
|
||||
| دابلبوکینگ بین پنل و سایت | بند ۸، پیش از هر روشنکردن |
|
||||
| نشت دستگاه داخلی به سایت | پیشفرض `online_bookable = 0` |
|
||||
| مبلغ دستکاریشده از client | مبلغ فقط سمت سرور از `appointment_uuid` مشتق میشود |
|
||||
| اختلاف عددِ سایت با فاکتور کلینیک | درصد روی قیمت پایه؛ فاکتور کامل حضوری |
|
||||
| پزشک ناخواسته | نمایش نام پزشک پیش از پرداخت + امکان رها کردن رزرو |
|
||||
| رزرو ناشناس و اشغال صندلی | رزرو موقت لاگین میخواهد |
|
||||
| استرداد غیرقابل تست | مسیر پیشفرض اعتبار است، نه درگاه |
|
||||
| تناقض قیمت offering با قیمت سرویس | `ResourceServiceOffering.price_rials` در این مسیر خوانده نمیشود؛ تصمیم دربارهٔ حذف یا رسمیکردنش خارج از این فاز |
|
||||
|
||||
---
|
||||
|
||||
## ۱۴. ترتیب اجرا
|
||||
|
||||
هر گام مستقلاً قابل تست و قابل deploy است.
|
||||
|
||||
**گام ۰ — یکیشدن اشغال**
|
||||
سرویس مشترک نوشتن اشغال. مهاجرت backfill. تست دو-مسیره.
|
||||
بدون این، هیچ گام دیگری نباید روی prod برود.
|
||||
|
||||
**گام ۱ — پرچمها**
|
||||
`online_bookable` روی منبع. `online_deposit_percent` روی سرویس و تنظیمات محیط.
|
||||
دو سوییچ در پنل. هنوز هیچ چیزی عمومی نیست.
|
||||
|
||||
**گام ۲ — سه اندپوینت عمومی**
|
||||
سرویسها، جستجو، رزرو موقت. با تست مالکیت محیط و تست منبعِ غیرآنلاین.
|
||||
|
||||
**گام ۳ — پرداخت**
|
||||
انتخاب مبلغ در `payment/appointment`. کسر از کیف پول. `gateway = wallet`.
|
||||
|
||||
**گام ۴ — سایت**
|
||||
مقدار سوم `booking_mode`. مسیر کلینیک. صفحهٔ تأیید. نمایش دو عدد.
|
||||
|
||||
**گام ۵ — لغو و اعتبار**
|
||||
قاعدهٔ ثابت پنجرهٔ رایگان. ردیف اعتبار. تب اعتبار در داشبورد.
|
||||
|
||||
قواعد ثابت پروژه که در هر گام رعایت میشوند:
|
||||
|
||||
- هیچ گامی بدون تست موفق و خطا و مرزی تمام نیست.
|
||||
- هر تغییر route یا شکل پاسخ، همان جلسه در `docs/api/*` مینشیند.
|
||||
- entity تازه یا `TenantOwnedTrait` میگیرد یا با دلیل در `GlobalTables` ثبت میشود.
|
||||
- دادهٔ محیط دیگر `404` میگیرد، نه `403`.
|
||||
Reference in New Issue
Block a user