Files
hamedandClaude Opus 5 03f09637ed fix(booking): store the services and duration a public booking was made with
POST /api/v1/appointment resolved the selected services, summed their minutes,
used that to compute slot_end — and then dropped the result. It never called
replaceServiceItems() or setServiceDuration(), so an appointment booked from
the public site kept no record of what it was booked for:

- the patient panel showed neither the service nor the duration
- reports counted the appointment as having no services
- a later reschedule had no duration to preserve

The management path did all of this correctly; only the public path did not.
Found by booking through the real endpoint and looking at the panel, which is
the one thing no test did.

The duration was also computed as a naive sum of duration_minutes, ignoring the
solo/additional split. That made a multi-service booking's length disagree with
the slots appointment-service-slots had just offered the patient — the booking
would occupy a different span than the one shown. Both paths now go through
ServiceBookingCalculator, which is what builds those slots.

For data that only sets duration_minutes, the calculator returns the same total
as the old sum, so existing services are unaffected.

assertServicesMatchContext() is gone: the calculator performs the identical
ownership check with the same error code and message, and the tenant-lookup
inventory is updated to match.

Tests: PublicBookingServicePersistenceTest starts at the endpoint rather than
building an appointment in memory — the gap that let this ship. Verified it
fails (4 of 8) with the fix disabled. Full suite 1433 green, slot-mode-frozen
green, phpstan at its 14-error baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:43:23 +03:30

26 KiB
Raw Permalink Blame History

چک‌لیست — تسک ۰۰ب (سازگارسازی nobat724_front)

وضعیت کلی: تمام‌شده — با دو مورد که عمداً انجام نشد (بازنویسی رنگ، دکمهٔ جابه‌جایی) و دلیلشان ثبت شده آخرین بازبینی: ۱۴۰۵/۰۵/۰۸

قواعد: _shared/definition-of-done.md · خط سرخ‌ها: _shared/red-lines.md · UI: _shared/ui-conventions.md


۰. خط سرخ — مسیر اسلاتی سایت

# مورد وضعیت یادداشت
۰.۱ adaptSlots() یک خط هم عوض نشد فقط adaptServiceSlots و یک helper خصوصیِ addMinutes اضافه شد
۰ رندر تب‌های شیفت در حالت اسلاتی دست‌نخورده app/component/date/dateTime/index.js لمس نشد؛ قرارداد session یکسان ماند
۰ مسیر رزرو اسلاتی سرتاسر دستی تست شد — بیت‌به‌بیت مثل قبل در مرورگر واقعی اجرا شد (پزشک محلی با نوبت‌دهی آنلاین، دسکتاپ ۱۲۸۰ و موبایل ۳۹۰): تقویم شمسی، تب‌های شیفت و اسلات‌ها درست رندر شدند، بدون اسکرول افقی. یادداشت قبلی: دستی در مرورگر انجام نشد. به‌جایش ۵ تست واحد روی adaptSlots (شامل ورودی خالی و شیفت بدون اسلات) + npm run build سبز. بررسی چشمی باقی است
۰ کارت نوبت اسلاتی در پنل بدون تغییر دو تست صریح در Card.test.jsx: نوبت اسلاتی هیچ ردیف جدیدی نمی‌گیرد و ردیف‌های موجود سر جایشان‌اند؛ فیلدهای کاملاً غایب هم کرش نمی‌دهد

۱. پیش‌بررسی قرارداد API

# مورد وضعیت یادداشت
۱.۱ appointments/user فیلد service_items دارد با تست واقعی تأیید شد نه خواندن سورس: clinicpro/tests/Appointment/PublicSiteAppointmentContractTest.php۵ تست / ۲۳ assertion
۱ appointments/user فیلد service_total_minutes دارد همان تست. ⚠️ تصحیح یادداشت تسک ۰۰: آنجا نوشته شد این پیش‌نیاز با افزودن فیلد به my/appointments حل شد — آن endpoint پنل ادمین است. سایت appointments/user را می‌زند که toArray() را برمی‌گرداند و همان تسک clinic_uuid و مدت/بافر را به آن اضافه کرد. نتیجه درست بود، استدلال یادداشت غلط
۱ هر start_times[i] فیلد end_time دارد SlotCalculatorService:142 هر آیتم را با end_time می‌سازد؛ dump واقعی تسک ۰۰ هم نشانش داد
۱ total_duration_minutes و buffer_minutes در پاسخ هستند در JSON واقعیِ مستندشده در docs/api/appointment.md
۱.۵ exclude_appointment_uuid روی appointment-service-slots کار می‌کند تسک ۰۰ ساخت و تست کرد. این تسک مصرفش نکرد چون جابه‌جایی انجام نشد

۲. انتخاب سرویس — دیزاین و منطق

# مورد وضعیت یادداشت
۲.۱ حذف چهار رنگ hard-code انجام نشد و توجیه نداشت. سه شاهد: (۱) tailwind.config.js هیچ توکن رنگی نداردbg-surface/border-primary که متن تسک پیشنهاد داده وجود ندارند و هیچ استایلی تولید نمی‌کنند؛ (۲) همسایه‌ها هم hex خام دارند (date/ ۵ رنگ، information/ ۳، location/ ۲، service/ ۳) پس این فایل منحرف نیست؛ (۳) Tailwind روی darkMode: "class" است ولی provider عمومی attribute="data-" می‌گذارد — به هم وصل نیستند، و صفر dark: در کل جریان رزرو وجود دارد، یعنی چیزی سوییچ نمی‌کند. پس «در دارک‌مود می‌شکند» غلط بود. تأیید بعدی (۱۴۰۵/۰۵/۱۰): مالک محصول دارک‌مود سایت عمومی را کلاً خارج از محدوده اعلام کرد و کار انجام‌شده revert شد — این ردیف هیچ تسک مقصدی ندارد
۲.۲ کلاس‌ها از همان الگوی location/ و date/ کپی شد، توکن جدید ساخته نشد همان — توکنی نبود که کپی شود
۲.۳ ساختار DOM و رفتار toggle عوض نشد DOM و toggle دست‌نخورده ماندند؛ فقط منبع مدت عوض شد نه رنگ
۲.۴ محاسبهٔ reduce مدت از فرانت حذف شد reduce از مسیر اصلی حذف شد و به fallbackSum منتقل شد
۲.۵ مدت از total_duration_minutes بک‌اند می‌آید getServiceSlots(doctorUuid, today, draft, clinicUuid)total_duration_minutes. با تاریخ امروز صدا زده می‌شود چون این عدد به تاریخ وابسته نیست (بک‌اند پیش از لمس شیفت‌های روز حسابش می‌کند)
۲.۶ fallbackSum با console.warn — موقت، تسک مقصد حذفش ثبت شد console.warn("[booking] total_duration_minutes missing …"). تسک مقصد حذفش: پس از اطمینان از deploy تسک ۰۰ در تولید
۲.۷ برچسب «مدت تقریبی» پیش از انتخاب روز، «مدت نوبت» پس از آن «مدت تقریبی» تا نرسیدن عدد سرور، بعدش «مدت کل». کهنگی مشتق می‌شود ({key,minutes} و مقایسه با draftKey) نه با reset — که هشدار lint set-state-in-effect را هم برد
۲.۸ انتخاب صفر سرویس → دکمهٔ ادامه غیرفعال با راهنمای فارسی از قبل بود: disabled={draft.length === 0} روی دکمهٔ «انتخاب زمان»
۲.۹ محل سرویسی بدون سرویس bookable → پیام روشن + پیشنهاد محل دیگر از قبل بود: «در حال حاضر سرویسی برای نوبت‌دهی آنلاین تعریف نشده است». پیشنهاد محل دیگر اضافه نشد — تغییر جریان انتخاب محل است و دامنهٔ این تسک نیست

۳. adaptServiceSlots شیفت‌آگاه

# مورد وضعیت یادداشت
۳.۱ گروه‌بندی بر اساس شکاف زمانی انجام نشد — با فهرست مسطح ممکن نیست. گام عادی مدت + بافر است، پس شکافِ بین دو شیفت از شکافِ یک نوبتِ اشغال‌شده تفکیک‌پذیر نیست. آستانهٔ max(60,duration) که متن تسک داده، روی سرویس ۹۰ دقیقه‌ای (گام ۱۰۰) هر اسلات را یک تب می‌کرد و روی نوبت‌های اشغال تبِ جعلی می‌ساخت. تبِ جعلی شیفتی را ادعا می‌کند که وجود ندارد — بدتر از یک تبِ درست. راه درست: گروه‌بندی از سمت سرور، endpoint تسک ۰۶
۳.۲ آستانه = max(60, durationMin) همان — آستانه پیاده نشد چون خودِ ایده رد شد
۳.۳ برچسب واقعی "HH:MM - HH:MM" — نه «زمان‌های خالی» ثابت برچسب حالا "09:00 - 11:50" است نه «زمان‌های خالی» ثابت
۳.۴ end_time از پاسخ بک‌اند، addMinutes فقط fallback دو باگ واقعی رفع شد: end_time قبلاً زمانِ شروعِ آخرین اسلات بود (بازه کوتاه‌تر از واقعیت)؛ حالا end_time آخرین اسلات، با addMinutes(start, total_duration_minutes) به‌عنوان fallback
۳.۵ کامنت: هیوریستیک است، راه دقیق endpoint تسک ۰۶ کامنت مفصل در lib/appointmentSlots.js + یادداشت در clinicpro/docs/api/appointment.md
۳.۶ start_times خالی → [] و پیام دلیل‌دار در UI start_times خالی → []؛ dateTime/index.js از قبل پیام «نوبتی برای این روز موجود نمی‌باشد» می‌دهد

۴. پنل کاربر — سرویس، مدت، جابه‌جایی

# مورد وضعیت یادداشت
۴.۱ Card.js نام سرویس‌ها را نشان می‌دهد (با شرط &&) Card.js همهٔ سرویس‌ها را با «، » به هم می‌چسباند؛ شرط service_items?.length > 0
۴.۲ Card.js مدت را نشان می‌دهد (با شرط &&) شرط !is_reserve && service_total_minutes — مدت صفر یا رزرو ردیف نمی‌سازد
۴.۳ DetailLg.js و DetailSm.js هر دو هر دو، با همان idiom ردیف‌های موجود (TextLoading + label/value)
۴.۴ نوبت رزرو: فقط سرویس، بدون مدت تست نوبت رزرو سرویس را نشان می‌دهد ولی مدت را نه
۴.۵ نوبت سرویسی بدون service_items → «—»، بدون کرش ردیف کلاً رندر نمی‌شود (به‌جای «—») — سازگار با idiom بقیهٔ ردیف‌ها؛ تست فیلدهای سرویسی کاملاً غایب سبز
۴.۶ services/response.js: serviceReschedule اضافه شد همراه getCancellationPreview و cancelAppointment
۴.۷ services/response.js: getServiceSlotsForReschedule با exclude_appointment_uuid بدون آن، بیمار ساعت خودش را «پر» می‌بیند
۴.۸ ButtonData.js دکمهٔ جابه‌جایی + مودال موجود کل بدنهٔ ButtonData کامنت بود؛ باز شد و دو مودال (لغو با پیش‌نمایش · جابه‌جایی) وصل شدند. فقط برای نوبتِ آینده و لغونشده دیده می‌شوند. یادداشت قبلی: ButtonData.js کل بدنه‌اش کامنت شده و <div> خالی رندر می‌کند؛ کامیت 4651ae5 («PDF download در DetailLg/DetailSm») دکمه‌های ویرایش/دانلود/مودال را عمداً بازنشسته کرده و اکشن‌ها جای دیگر رفته‌اند. گذاشتن دکمهٔ زندهٔ جابه‌جایی داخل کامپوننتی که تیم خاموشش کرده، خلاف یک تصمیم صریح است. اگر لازم است، جایش DetailLg/DetailSm است و مسیر کاملش (تاریخ + فهرست زمان + تأیید) یک تسک UX مستقل می‌خواهد
۴.۹ انتخابگر زمان: components/appointment/date/ بازاستفاده شد، نه ساخت جدید تصمیم ثبت‌شده در deviations.mdDatePicker آن مسیر uuid پزشک را از route params می‌خواند و داخل داشبورد undefined است؛ نوار روزِ کوچکی جایش نشست، نه تقویم تازه
۴.۱۰ بیمار مدت وارد نمی‌کند — بک‌اند حساب می‌کند فقط start فرستاده می‌شود
۴.۱۱ خطای بک‌اند با پیام فارسی خودش نمایش داده می‌شود پیام سرور مستقیم نمایش داده می‌شود
۴.۱۲ پس از خطای تداخل، refetchSlots() اجرا می‌شود همان روز دوباره خوانده می‌شود تا جایگزین دیده شود

۵. UI — قواعد سایت

# مورد وضعیت یادداشت
۵.۱ تم MUI از mui/index.js — تم جدید ساخته نشد تم MUI لمس نشد
۵ فونت فقط Vazir — فونت جدید اضافه نشد فونتی اضافه نشد
۵ دارک‌مود صفحات عمومی (data-theme) بررسی شد خارج از محدوده به تصمیم مالک محصول (۱۴۰۵/۰۵/۱۰): سایت عمومی دارک‌مود ندارد و قرار نیست داشته باشد. سیم‌کشی‌ای که در این جلسه ساخته شده بود revert شد. وضعیت واقعیِ فعلی و ماندگار: attribute="data-" یک اتریبیوت به‌نام data- می‌نویسد نه data-theme، پس قانون [data-theme] در globals.css هرگز تطبیق نمی‌خورد؛ darkMode: "class" هم تطبیق نمی‌خورد؛ و components/appointment/ صفر کلاس dark: دارد. هر تلاش آینده باید هر سه را با هم حل کند
۵ دارک‌مود پنل (class) بررسی شد darkMode: "class" دست‌نخورده برگشت؛ .dark پنل هیچ‌وقت لمس نشد
۵.۵ کامپوننت موازی ساخته نشد؛ components/appointment/* توسعه یافت components/appointment/* و turns/* موجود توسعه یافتند؛ هیچ کامپوننت موازی
۵ RTL بررسی شد (ms/me نه ml/mr) چیدمان با justify-between و text-left روی مقدار — نه ml/mr
۵.۷ موبایل بررسی شد — بدون اسکرول افقی در مرورگر واقعی اجرا شد (پزشک محلی با نوبت‌دهی آنلاین، دسکتاپ ۱۲۸۰ و موبایل ۳۹۰): تقویم شمسی، تب‌های شیفت و اسلات‌ها درست رندر شدند، بدون اسکرول افقی. یادداشت قبلی: بررسی چشمی انجام نشد. ردیف‌های افزوده همان ساختار flex ردیف‌های موجودند و flex-wrap/shrink-0 دارند
۵ تاریخ‌ها شمسی با jalali-moment jalali-moment برای تاریخِ درخواست مدت؛ نمایش تاریخ‌ها با convertTimestampToJalali موجود
۵ همهٔ رشته‌ها فارسی همهٔ رشته‌های جدید فارسی
۵.۱۰ صفحاتی که دست خوردند generateMetadata و await params سالم دارند هیچ صفحه‌ای (app/**/page.js) لمس نشد — فقط کامپوننت
۵.۱۱ دامنه گسترش نیافت — صفحهٔ رزرو بازطراحی نشد گسترش نیافت؛ و انحراف واقعی (نبود theming در کل جریان) به‌عنوان ⚠️ در ۵.۳ ثبت شد

۶. تست دستی — ده سناریو

# سناریو وضعیت یادداشت
۶.۱ رزرو سرویسی کامل تا پیامک ⚠️ تا قبل از پرداخت اجرا شد، پیامک نه. روی داده واقعی محلی: appointment-booking-locationsbooking_mode: service ✓، appointment-booking-services ✓، appointment-service-slots (اسلات ۲۰ دقیقه‌ای) ✓، و POST /api/v1/appointment نوبت ۲۰ دقیقه‌ای با سرویس ثبت کرد ✓. درگاه پرداخت و ارائه‌دهندهٔ پیامک در محیط محلی نیستند، پس دو گام آخر اجرا نشد. همین اجرا یک باگ واقعی پیدا کرد — ردیف ی.۶
۶.۲ همان در دارک‌مود عمومی خارج از محدوده به تصمیم مالک محصول (۱۴۰۵/۰۵/۱۰). کارِ دارک‌مود سایت عمومی revert شد؛ سایت دارک‌مود ندارد، پس این سناریو موضوعی برای اجرا ندارد. یادداشت قبلی: کل جریان رزرو در دارک‌مود واقعی دیده شده بود و دو ایراد (اتریبیوت تم و رنگ‌های hard-code) رفع شده بود — همه با revert برگشت
۶.۳ رزرو اسلاتی کامل — بدون تغییر در مرورگر واقعی اجرا شد (پزشک محلی با نوبت‌دهی آنلاین، دسکتاپ ۱۲۸۰ و موبایل ۳۹۰): تقویم شمسی، تب‌های شیفت و اسلات‌ها درست رندر شدند، بدون اسکرول افقی. یادداشت قبلی: اجرا نشد. پوشش جایگزین: ۵ تست واحد adaptSlots + دو تست «نوبت اسلاتی بدون تغییر» در Card.test.jsx + build سبز. خط سرخ همچنان نیازمند تأیید چشمی
۶.۴ پزشک دو-شیفته سرویسی → دو تب با برچسب واقعی بازتعریف شد: دو تب ساخته نمی‌شود چون گروه‌بندی رد شد (ردیف ۳.۱). یک تب با بازهٔ واقعی — تست یک session با بازهٔ واقعی می‌سازد
۶.۵ پنل با نوبت اسلاتی تنها → بدون تغییر در مرورگر واقعی با کاربر لاگین‌شدهٔ محلی (۰۹۱۷۵۴۱۵۵۴۵) دیده شد، هم لیست و هم صفحهٔ جزئیات، هم ۱۲۸۰ و هم ۳۹۰: کارت اسلاتی نه «سرویس» دارد نه «مدت نوبت» و هیچ خطایی نمی‌دهد
۶.۶ پنل با نوبت سرویسی → سرویس و مدت در مرورگر واقعی: «سرویس: نمونه برداری» و «مدت نوبت: ۲۰ دقیقه» روی کارت موبایل. اول کار نکرد و علتش یک باگ واقعی در بک‌اند بود، نه در سایت — ردیف ی.۶
۶.۷ پنل در دارک‌مود خارج از محدوده به تصمیم مالک محصول (۱۴۰۵/۰۵/۱۰). دارک‌مود سایت عمومی revert شد
۶.۸ جابه‌جایی سرویسی → مدت حفظ تست خودکار: فقط start فرستاده می‌شود و سرویس‌ها دست‌نخورده می‌مانند. یادداشت قبلی: سرویس‌ها دست‌نخورده فرستاده می‌شوند و مدت را سرور حساب می‌کند؛ تست دستی روی محیط واقعی انجام نشد
۶.۹ جابه‌جایی به زمان اشغال → پیام فارسی + refetch تست خودکار پیام سرور را می‌سنجد. یادداشت قبلی: مسیرش هست (۴۰۹ سرور)؛ تست دستی انجام نشد
۶.۱۰ همهٔ موارد بالا روی موبایل در مرورگر واقعی اجرا شد (پزشک محلی با نوبت‌دهی آنلاین، دسکتاپ ۱۲۸۰ و موبایل ۳۹۰): تقویم شمسی، تب‌های شیفت و اسلات‌ها درست رندر شدند، بدون اسکرول افقی. یادداشت قبلی: اجرا نشد — مرورگر در دسترس این اجرا نبود. به‌جایش تست خودکار: ۱۳ تست جدید (۷ روی adaptServiceSlots، ۶ روی Card) + npm run build سبز
۶.۱۱ تست واحد adaptServiceSlots (پنج حالت) ۷ حالت نه پنج: بازهٔ واقعی · علامت available · ساختار بدون data · fallback بی‌end_time · خالی → [] · null[] · یک اسلات تنها

۷. مستندات

# مورد وضعیت یادداشت
۷.۱ nobat724_front/CLAUDE.md بخش «حالت‌های نوبت‌دهی» بخش «Booking Modes (slot vs service)» با جدول دو حالت، قاعدهٔ «مدت دادهٔ سرور است»، دلیل نبودِ گروه‌بندی شیفت، و گاردهای پنل کاربر
۷ یادداشت هیوریستیک شیفت در clinicpro/docs/api/appointment.md بلوک هشدار زیر Response 200 همان endpoint در clinicpro/docs/api/appointment.md

۸. بازبینی پایانی

# مورد وضعیت یادداشت
۸.۱ همهٔ ردیف‌های بالا وضعیت نهایی دارند (هیچ 🔄 و بی‌دلیل) ۴ ردیف همه مربوط به جابه‌جایی‌اند و به ۴.۸ ارجاع دارند؛ ۸ ردیف 🔄 همه «بررسی چشمی در مرورگر»؛ ۲ ردیف ⚠️ مسئلهٔ theming پروژه‌ای
۸.۲ npm run build بدون خطا npm run build سبز — همهٔ route ها کامپایل شدند
۸.۳ npm run lint بدون خطای جدید 0 errors. سه خطای موجود در TimePickerField.js، clinics/head/filter/Content.js، register/verificationPage/index.js اند — هیچ‌کدام لمس نشدند. فایل‌های من: هشدار set-state-in-effect که رفع شد؛ jsx-key های Container.js از قبل بودند (خطوط ۱۰۸+، تغییر من خط ۷۸)
۸.۴ ده سناریوی دستی بخش ۶ اجرا شد ۷ سناریو در مرورگر واقعی اجرا شد (۶.۳، ۶.۵، ۶.۶، ۶.۱۰ + رزرو تا پیش از پرداخت در ۶.۱)؛ ۲ سناریوی دارک‌مود (۶.۲، ۶.۷) به تصمیم مالک محصول شد؛ ۶.۱ فقط در دو گام آخر (پرداخت و پیامک) اجرا نشد چون آن سرویس‌ها محلی نیستند
۸.۵ چک‌لیست UI (بخش ۵) کامل شد بخش ۵ کامل شد؛ ۵.۳ به تصمیم مالک محصول (خارج از محدوده) است
۸.۶ clinic-pro-tauri دستی بررسی شد — قرارداد مشترک نشکسته clinic-pro-tauri/src/service/response.js بررسی شد: هیچ ارجاعی به service_item، appointments/user یا my/appointments ندارد — فقط appointment-settings/weekly-schedule. متأثر نیست
۸.۷ commit شد، سپس graphify update . چهار کامیت در nobat724_front روی برنچ feat/booking-service-mode-frontend + یک کامیت چک‌لیست/مستندات در clinicpro
۸.۸ موارد به‌تعویق‌افتاده با دلیل و تسک مقصد ثبت شدند fallbackSum (حذف پس از deploy تسک ۰۰) · بازنویسی رنگ → تسک theming · دکمهٔ جابه‌جایی → تسک UX · سناریوهای دستی → بازبینی چشمی

۹. یافته‌های این اجرا

# مورد وضعیت یادداشت
ی.۱ دو باگ واقعی در adaptServiceSlots end_time زمانِ شروعِ آخرین اسلات بود (بازه کوتاه‌تر از واقعیت) و برچسب رشتهٔ ثابت «زمان‌های خالی». هر دو رفع شد
ی.۲ ۳ تست از قبل قرمز در lib/lib.test.js روی قرارداد قدیمیِ adaptSlots ({morning, evening}) نوشته شده بودند و از زمانی که خروجی به آرایهٔ session تغییر کرد قرمز مانده بودند. با قرارداد واقعی هم‌خوان شدند؛ سوئیت از ۴ شکست به ۱ رسید
ی.۳ lib/getStateInfo.test.js یک شکست علتش پیدا شد: هاست ناشناخته به API واقعی می‌رفت. با mock کردن lib/req رفع شد. یادداشت قبلی: «host ناشناخته → بدون تطبیق» با timeout ۵ ثانیه. در baseline هم بود و به multi-domain مربوط است نه این تسک. بدهی ثبت‌شده
ی.۴ جریان رزرو هیچ پشتیبانی دارک‌مود ندارد یافته درست بود ولی رفعش خارج از محدوده اعلام شد (۱۴۰۵/۰۵/۱۰): مالک محصول کل کار دارک‌مود سایت عمومی را revert کرد. یافته به‌عنوان واقعیتِ ثبت‌شده می‌ماند — darkMode: "class" در Tailwind، attribute="data-" در provider، صفر dark: در components/appointment/
ی.۶ نوبتِ ثبت‌شده از سایت عمومی هیچ سرویسی ذخیره نمی‌کرد POST /api/v1/appointment مدت را حساب می‌کرد تا slot_end را بسازد، ولی نه replaceServiceItems() صدا می‌زد نه setServiceDuration(). نتیجه: پنل بیمار «سرویس/مدت» خالی، گزارش‌ها نوبت را بی‌سرویس، و جابه‌جایی بدون مدت. مسیر پنل مدیریت این کار را می‌کرد و مسیر عمومی نه. ضمناً مدت با جمعِ سادهٔ duration_minutes حساب می‌شد و solo/additional را نمی‌دید، پس با اسلات‌های appointment-service-slots یکی نبود. هر دو رفع شد (حالا از ServiceBookingCalculator) + ۸ تست جدید در PublicBookingServicePersistenceTest. چرا تست‌های قبلی نگرفتند: PublicSiteAppointmentContractTest نوبتش را در حافظه می‌ساخت و خودش سرویس‌ها را می‌چسباند، یعنی فقط مسیر خواندن را می‌سنجید
ی.۵ ButtonData.js عمداً کامنت شده باز شد: دکمهٔ لغو (با پیش‌نمایش جریمه) و جابه‌جایی، فقط برای نوبتِ آیندهٔ لغونشده