31 KiB
نوبتدهی منبعمحورِ آنلاین با بیعانهٔ درصدی
دامنه:
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، در ساخت الزامی). نوبتِ منبع پزشکش را از همینجا میگیرد. - تضمین ضدتداخل در دیتابیس است، با کلید یکتای زیر روی سطلهای پنجدقیقهای:
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
ALTER TABLE clinic_resources
ADD COLUMN online_bookable TINYINT(1) NOT NULL DEFAULT 0;
پیشفرض 0 عمدی است. دستگاه داخلی و اتاق عمل نباید با یک deploy به سایت نشت کنند.
۵.۲ service_items
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است
پاسخ:
{
"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
بدنه:
{ "service_uuid": "…", "from": 1785529800, "to": 1785616200, "item_uuids": [] }
همان قواعد اندپوینت خصوصی: هر دو سر بازه شامل، سقف ۹۰ روز. تفاوتها:
- فقط منابع
online_bookableدر جستجو شرکت میکنند. assignmentدر پاسخ برنمیگردد. بیمار منبع را نمیبیند.- بهجایش هر اسلات
doctorدارد:{ uuid, name }از ناظرِ منبعِ پیشنهادی.
{
"success": true,
"data": {
"slots": [
{ "start": 1785562200, "end": 1785565200,
"doctor": { "uuid": "…", "name": "امیر کاظمی" } }
]
}
}
POST …/hold
لاگین لازم است. تنها اندپوینت از این سه که JWT میخواهد. دلیل: رزرو موقت صندلی میگیرد، و ناشناسی که صندلی میگیرد قابل پاسخگویی نیست.
پاسخ همان hold_uuid و بازه و expires_at است، بهعلاوهٔ:
{
"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
یک فیلد تازه که فرانت با آن میفهمد کدام عدد را نشان بدهد:
{ "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.