refactor(booking): extract ServiceBookingCalculator from the controller

"Allowed duration of a service combination" lived inside
AppointmentController::serviceSlots(). Three upcoming callers need the same
computation (PATCH duration validation, service-aware reschedule, reserve
conversion); copying it would mean four variants with four different edge-case
behaviours.

The extraction is behaviour-preserving: BaseController::error() and
ExceptionSubscriber emit an identical envelope, so returning $this->error() was
replaced by throwing AppException with the same code/message/field.

Tenant ownership now goes through TenantOwnershipChecker::belongsToPair() (the
documented single point) instead of an inline section pair comparison. The repo
property is named itemRepo on purpose: TenantLookupInventoryTest only counts
recognised property names, so any other name would slip past the safety net.

The naive duration sum is kept deliberately — switching to solo/additional
minutes is task 04 and changes one line here.

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:41:13 +03:30
co-authored by Claude Opus 5
parent 113e8d93a0
commit 6afc5c090e
6 changed files with 437 additions and 40 deletions
@@ -0,0 +1,32 @@
<?php
namespace App\Appointment\ValueObject;
use App\ClinicService\Entity\ServiceItem;
/**
* نتیجهٔ محاسبهٔ مدت یک ترکیب سرویس در حالت نوبت‌دهی سرویسی.
*
* `bufferMinutes` فاصلهٔ بین دو نوبت است و **جزو مدت نوبت نیست**: زمان پایانِ ذخیره‌شدهٔ
* نوبت `start + totalMinutes` است، نه `start + totalMinutes + buffer`. همان قاعده‌ای که
* {@see \App\Appointment\Service\SlotCalculatorService::getServiceStartTimes()} دارد.
*/
final readonly class ServiceBookingDuration
{
/**
* @param ServiceItem[] $serviceItems به ترتیب uuidهای ورودی
* @param string[] $warnings پیام‌های فارسی برای نمایش؛ مانع عملیات نیستند
*/
public function __construct(
public int $totalMinutes,
public int $bufferMinutes,
public array $serviceItems,
public array $warnings = [],
) {}
/** زمان پایان نوبت برای یک زمان شروع مشخص — بدون بافر. */
public function endFor(int $slotStart): int
{
return $slotStart + $this->totalMinutes * 60;
}
}