feat: add PublicResourceBookingController and PublicResourceBookingService for public booking functionality

- Implemented PublicResourceBookingController to handle public resource booking requests.
- Added methods for retrieving bookable resources, available slots, and month availability.
- Created PublicResourceBookingService to manage public resource offerings and service visibility.
- Developed tests for public resource booking to ensure correct functionality and error handling.
This commit is contained in:
hamed
2026-08-09 10:45:51 +03:30
parent cd793489ef
commit cfeb447645
10 changed files with 1645 additions and 13 deletions
@@ -52,6 +52,8 @@ class AppointmentController extends BaseController
private readonly \App\Appointment\Service\ServiceBookingCalculator $serviceCalculator,
private readonly \App\Appointment\Service\ServiceRescheduleService $rescheduleService,
private readonly \App\Appointment\Availability\Service\ResourceOccupier $occupier,
private readonly \App\Resource\Service\ResourceBookingSlotService $resourceSlots,
private readonly \App\Resource\Service\PublicResourceBookingService $publicResources,
private readonly \App\Treatment\Service\SessionBookingLink $sessionLink,
private readonly \Psr\Log\LoggerInterface $logger,
) {}
@@ -523,6 +525,22 @@ class AppointmentController extends BaseController
$bookingClinic = $this->bookingClinic($doctor, $clinicUuid);
if ($resource !== null) {
/**
* منبع با uuid از بدنهٔ درخواست می‌آید و `TenantFilter` پوششش نمی‌دهد، پس
* بدون این بررسی بیمار می‌توانست دستگاه کلینیک دیگری را روی نوبت این کلینیک
* بنشاند.
*
* پیش از محاسبهٔ مدت می‌آید تا منبعِ بیگانه همان «منبع یافت نشد» را بگیرد، نه
* خطای ریزترِ سرویس را.
*/
[$bookingType, $bookingId] = EntityContext::forBooking($doctor, $bookingClinic)->toEntityPair();
if ($resource->getEntityType() !== $bookingType || $resource->getEntityId() !== $bookingId) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'منبع یافت نشد', 422, 'resource_uuid');
}
}
/**
* مدت از {@see ServiceBookingCalculator} می‌آید، نه از جمعِ دستیِ `duration_minutes`.
*
@@ -531,25 +549,25 @@ class AppointmentController extends BaseController
* می‌کرد که سرور جای دیگری آزاد حساب کرده بود. calculator خودش هم مالکیت محیط را
* می‌سنجد (همان بررسی‌ای که قبلاً جداگانه صدا زده می‌شد) و خطاهایش همان کد و پیام
* قبلی را دارند.
*
* **استثنای منبع:** وقتی منبع همهٔ این سرویس‌ها را عمومی ارائه می‌دهد، مدت از
* زنجیرهٔ حلِ خودِ منبع می‌آید ({@see ResourceServiceResolver}) — همان عددی که
* `appointment-resource-slots` با آن زمان‌ها را ساخته. وگرنه `slot_end` با اسلاتی
* که به بیمار نشان داده شد یکی نمی‌شد.
*
* شرطی است و نه همیشگی: محیطی که هنوز رابطه‌های منبع↔سرویس را پر نکرده باید مثل
* قبل کار کند.
*/
$duration = null;
if ($hasServices) {
$duration = $this->serviceCalculator->calculate($doctor, $bookingClinic, $serviceUuids);
$slotEnd = $duration->endFor($slotStart);
$duration = $resource !== null && $this->publicResources->offersAllPublicly($resource, $serviceUuids)
? $this->resourceDuration($resource, $serviceUuids)
: $this->serviceCalculator->calculate($doctor, $bookingClinic, $serviceUuids);
$slotEnd = $duration->endFor($slotStart);
}
if ($resource !== null) {
/**
* منبع با uuid از بدنهٔ درخواست می‌آید و `TenantFilter` پوششش نمی‌دهد، پس
* بدون این بررسی بیمار می‌توانست دستگاه کلینیک دیگری را روی نوبت این کلینیک
* بنشاند.
*/
[$bookingType, $bookingId] = EntityContext::forBooking($doctor, $bookingClinic)->toEntityPair();
if ($resource->getEntityType() !== $bookingType || $resource->getEntityId() !== $bookingId) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'منبع یافت نشد', 422, 'resource_uuid');
}
/**
* منبعی که این سرویس را نمی‌دهد، همین‌جا رد می‌شود نه وقتی بیمار سرِ قرار
* حاضر شده. روی `serviceItems`ِ خروجی calculator کار می‌کند نه uuidهای خام:
@@ -898,6 +916,27 @@ class AppointmentController extends BaseController
return $this->bookingContext->resolve($doctor, $clinicUuid);
}
/**
* مدتِ نوبت روی یک منبع، در قالبِ همان VOیی که مسیر پزشک‌محور تولید می‌کند.
*
* هم‌شکل‌کردنشان عمدی است: پایین‌دستِ این نقطه (`replaceServiceItems`،
* `setServiceDuration`، `endFor`) دیگر نمی‌داند نوبت منبع‌دار است یا نه، و شاخهٔ
* دومی برای نگهداری ندارد.
*
* بافر صفر است چون `ResourceBookingSlotService::startTimes()` زمان‌ها را پشت‌سرهم و
* بدون بافر می‌چیند؛ عددِ دیگری اینجا یعنی واگرایی با همان اسلات‌ها.
*
* @param list<string> $serviceUuids
*/
private function resourceDuration(
\App\Resource\Entity\ClinicResource $resource,
array $serviceUuids,
): \App\Appointment\ValueObject\ServiceBookingDuration {
['minutes' => $minutes, 'items' => $items] = $this->resourceSlots->resolveDuration($resource, $serviceUuids);
return new \App\Appointment\ValueObject\ServiceBookingDuration($minutes, 0, $items);
}
/**
* آیا این درخواستِ اسلات از پنل مدیریت است (پزشک/منشی/ادمینِ دارای دسترسی)؟
* اندپوینت‌های اسلات عمومی‌اند؛ فقط با management=1 + کاربرِ احرازشده و مجاز،