feat(booking): make PATCH derive appointment duration from its services

In service mode PATCH accepted any duration and only updated the single
service_item column while the service_items collection stayed untouched, so an
edit could leave an appointment with old services and a new length. A 45-minute
service could be shortened to 20 and the next patient would sit on top of it.

Services are now resolved before the time block (duration depends on them) and
the stored end must equal start + total minutes. Reserve entries are exempt:
they carry slot_start == slot_end and occupy no interval, but they do store the
computed duration so a later conversion does not lose it.

No convert-reserve endpoint was added: PATCH already converts a reserve to a
timed appointment via rescheduleTo($start, $end, $isReserve), which refreshes
active_slot_key itself. Project rule 8 — a new endpoint needs an existing one to
be insufficient even after extension.

Slot mode is untouched: with booking_mode = slot the duration stays null and not
one of the new branches runs. Covered by an explicit test.

New error codes are ERR_APPOINTMENT_003/004 (the file only had 001/002).

Task: docs/new_feture/taskes/task-00-service-mode-completion/
Slot-mode contract: unchanged (--group=slot-mode-frozen green)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-30 12:57:48 +03:30
co-authored by Claude Opus 5
parent 2dae76cac0
commit b589a851d0
3 changed files with 309 additions and 0 deletions
+6
View File
@@ -34,6 +34,10 @@ class ErrorCodes
// Appointment
public const ERR_APPOINTMENT_001 = 'ERR_APPOINTMENT_001';
public const ERR_APPOINTMENT_002 = 'ERR_APPOINTMENT_002';
/** مدت نوبت با مجموع مدت سرویس‌های انتخابی نمی‌خواند (حالت نوبت‌دهی سرویسی). */
public const ERR_APPOINTMENT_003 = 'ERR_APPOINTMENT_003';
/** عملیات با روش نوبت‌دهیِ این محل سازگار نیست (slot در برابر service). */
public const ERR_APPOINTMENT_004 = 'ERR_APPOINTMENT_004';
// File
public const ERR_FILE_001 = 'ERR_FILE_001';
@@ -133,6 +137,8 @@ class ErrorCodes
self::ERR_PAYMENT_004 => 'محیط این پرداخت مشخص نیست',
self::ERR_APPOINTMENT_001 => 'اسلات انتخاب‌شده در دسترس نیست',
self::ERR_APPOINTMENT_002 => 'نوبت قابل لغو نیست',
self::ERR_APPOINTMENT_003 => 'مدت نوبت با مجموع مدت سرویس‌های انتخابی نمی‌خواند',
self::ERR_APPOINTMENT_004 => 'این عملیات با روش نوبت‌دهی این محل سازگار نیست',
self::ERR_FILE_001 => 'فرمت فایل مجاز نیست',
self::ERR_FILE_002 => 'حجم فایل بیش از حد مجاز است (حداکثر 5MB)',
self::ERR_SMS_001 => 'موجودی پیامک کافی نیست',