feat: add visit price requirement feature
- Introduced a new boolean flag `require_visit_price` in the `EntityInsurancePricing` to enforce visit price for appointments. - Updated the appointment creation endpoints to validate `visit_price_rials` based on the new flag. - Added `visit_price_rials` field to the `Appointment` entity to store the visit price. - Enhanced the `PatientService` to validate visit price during session creation. - Updated API documentation to reflect changes in appointment and insurance pricing. - Implemented a new service `VisitPriceRequirementResolver` to determine if a visit price is required for a doctor based on their pricing settings. - Added migrations to update the database schema for the new fields.
This commit is contained in:
@@ -474,6 +474,7 @@ Create a new appointment for a patient. Used by doctor/clinic/secretary to book
|
||||
| `patient_mobile` | string | ✅ | راه تماس بیمار |
|
||||
| `patient_name` | string | ✅ | نام بیمار — فقط برای بیمارِ **کاملاً جدید** استفاده میشود؛ اگر کد ملی به پروفایلِ موجود بخورد، نامِ همان پروفایل روی نوبت ذخیره و نمایش داده میشود و این ورودی نادیده گرفته میشود |
|
||||
| `patient_national_code` | string | ✅ | کد ملی بیمار — باید ۱۰ رقم معتبر باشد (`isValidIranNationalCode`)؛ ارقام فارسی به انگلیسی تبدیل میشوند |
|
||||
| `visit_price_rials` | int | شرطی | هزینه ویزیت (ریال). اختیاری؛ ولی اگر فلگ `require_visit_price` در [insurance-pricing](insurance.md) برای پزشک (یا کلینیکِ واحد او در نبود ردیف پزشک) فعال باشد، مقدار `> 0` الزامی است. روی نوبت ذخیره و در `toArray` با کلید `visit_price_rials` برمیگردد |
|
||||
|
||||
> **هویت بیمار بر پایهی کد ملی:** کد ملی روی **پروفایل** بیمار ذخیره میشود (`profiles.national_code`، یکتا). بیمار **اول با کد ملیِ پروفایل** پیدا میشود، سپس با موبایل. پس یک شخص میتواند چند موبایل داشته باشد ولی پروندهاش (`PatientRecord`) یکتا میماند. اگر موبایلی که پروفایلش کد ملی دیگری دارد دوباره با کد ملی متفاوت ارسال شود، خطای 422 برمیگردد. اگر هیچ بیماری یافت نشود، کاربر جدید (`ROLE_USER`) بههمراه پروفایلِ حاملِ همان کد ملی ساخته میشود. موبایلِ واردشده در هر نوبت بهصورت snapshot روی خودِ نوبت (`patient_mobile`) هم ذخیره میشود.
|
||||
>
|
||||
@@ -496,7 +497,7 @@ Create a new appointment for a patient. Used by doctor/clinic/secretary to book
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `FORBIDDEN` | 403 | Role not allowed, or caller not scoped to this doctor |
|
||||
| `VALIDATION` | 422 | Missing required fields, or missing/invalid `patient_national_code` (`field: patient_national_code`) |
|
||||
| `VALIDATION` | 422 | Missing required fields, or missing/invalid `patient_national_code` (`field: patient_national_code`), or required `visit_price_rials <= 0` when `require_visit_price` is on (`field: visit_price_rials`) |
|
||||
| `ERR_PROFILE_MOBILE_TAKEN` | 422 | این شماره موبایل با کد ملی دیگری ثبت شده است (`field: patient_mobile`) |
|
||||
| `DOCTOR_NOT_FOUND` | 404 | Doctor UUID not found |
|
||||
| `SLOT_TAKEN` | 409 | Slot already booked |
|
||||
@@ -597,7 +598,7 @@ Role-aware paginated list of appointments. Returns only what the authenticated u
|
||||
|
||||
## Clinic workflow extensions (نوبتها — Figma)
|
||||
|
||||
New optional fields on `Appointment` (all backward-compatible): `service_section` (بخش), `service_item` (سرویسِ اصلی/اول), `service_items` (آرایهٔ همهٔ سرویسهای نوبت — چند سرویس، هر عضو `{uuid, name}`), `staff` (پرسنل), `deposit_required` / `deposit_amount_rials` (بیعانه), `is_reserve` (نوبت رزرو — day-level, never occupies a slot).
|
||||
New optional fields on `Appointment` (all backward-compatible): `service_section` (بخش), `service_item` (سرویسِ اصلی/اول), `service_items` (آرایهٔ همهٔ سرویسهای نوبت — چند سرویس، هر عضو `{uuid, name}`), `staff` (پرسنل), `deposit_required` / `deposit_amount_rials` (بیعانه), `visit_price_rials` (هزینه ویزیت، nullable), `is_reserve` (نوبت رزرو — day-level, never occupies a slot).
|
||||
|
||||
New statuses: `following_up` (در حال پیگیری), `salon` (سالن). Transitions:
|
||||
`pending → confirmed|following_up|cancelled_*|expired` · `confirmed → completed|following_up|salon|cancelled_*|no_show` · `following_up → confirmed|salon|completed|cancelled_*|no_show` · `salon → completed|following_up|cancelled_*|no_show`
|
||||
@@ -631,7 +632,7 @@ Response `200`: `{ success, data: { data: <appointment.toArray()> } }`
|
||||
| 409 | slot taken or version conflict |
|
||||
|
||||
### POST `/api/v1/my/appointment` (extended)
|
||||
Extra optional body fields: `service_section_uuid`, `service_item_uuid`, `staff_uuid`, `deposit_required`, `deposit_amount_rials`, `is_reserve`, `service_item_uuids[]`, `duration_from_services`.
|
||||
Extra optional body fields: `service_section_uuid`, `service_item_uuid`, `staff_uuid`, `deposit_required`, `deposit_amount_rials`, `visit_price_rials`, `is_reserve`, `service_item_uuids[]`, `duration_from_services`.
|
||||
`is_reserve: true` → day-level reserve entry: `slot_end` may equal `slot_start`, the past-slot rule is skipped, and the entry never occupies a slot (several reserves may share a day). Response `201` now also returns `is_reserve`.
|
||||
`service_item_uuids[]` (غیرِ رزرو): یک یا چند سرویس که به نوبت **پیوست** میشوند (چند سرویس)؛ اولین سرویس = سرویسِ اصلی و همه در `service_items` برمیگردند. UUID ناموجود ⇒ `422`.
|
||||
`duration_from_services: true` (حالت نوبتدهی سرویسی): مدت نوبت از مجموع `duration_minutes` سرویسها محاسبه و `slot_end` بازنویسی میشود؛ در این حالت سرویسِ غیرbookable یا بدون مدت ⇒ `422`. بدون این پرچم (حالت اسلاتی)، ساعت پایانِ دستی حفظ میشود.
|
||||
|
||||
Reference in New Issue
Block a user