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:
@@ -0,0 +1,412 @@
|
||||
# نوبتدهی آنلاین منبعمحور — اندپوینتهای عمومی
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (Backend).
|
||||
|
||||
پرامپت همتا در سایت عمومی: `nobat724_front/.claude/prompt/public-resource-booking-ui.md`.
|
||||
**اول این را اجرا کن، بعد آن را.** قرارداد API که اینجا ساخته میشود مصرفکنندهٔ مستقیم دارد و
|
||||
تغییرش در build سایت خطا نمیدهد.
|
||||
|
||||
## زمینه
|
||||
|
||||
منبع (`ClinicResource`) هر چیزی است که ممکن است اشغال باشد: پزشک، اپراتور، دستگاه، اتاق، یونیت.
|
||||
هر منبع تقویم خودش را دارد و سرویسهایی که ارائه میدهد در `resource_service_offerings` ثبت شدهاند
|
||||
(`ResourceServiceOffering`)، با مدت و قیمتِ اختصاصیِ همان جفتِ منبع↔سرویس.
|
||||
|
||||
روی `ServiceItem` یک توگل به نام `bookable` هست که در پنل با برچسب «نمایش در نوبتدهی آنلاین»
|
||||
دیده میشود:
|
||||
|
||||
```php
|
||||
// src/ClinicService/Entity/ServiceItem.php:107
|
||||
/** نمایش این سرویس در نوبتدهی (پزشک ممکن است همهٔ سرویسها را ارائه ندهد). */
|
||||
#[ORM\Column(type: 'boolean', options: ['default' => false])]
|
||||
private bool $bookable = false;
|
||||
```
|
||||
|
||||
امروز این توگل فقط جریانِ **پزشکمحورِ** سایت را تغذیه میکند
|
||||
(`AppointmentController::bookableServices()` → `ServiceItemRepository::findBookableByEntity()`).
|
||||
هیچ مسیر عمومیای منابع را نمیبیند.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
اسلاتهای منبع فقط از داخل پنل قابل خواندناند:
|
||||
|
||||
```php
|
||||
// src/Resource/Controller/ResourceBookingSlotController.php:26
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
class ResourceBookingSlotController extends BaseController
|
||||
{
|
||||
use ResourcePermissionTrait;
|
||||
// هر اکشن با denyUnlessGrantedForBooking($user) گیت میشود
|
||||
```
|
||||
|
||||
`ResourceContext::resource($user, $uuid)` هم منبع را در محیطِ کاربرِ احرازشده حل میکند، پس برای
|
||||
بازدیدکنندهٔ ناشناسِ سایت اصلاً قابل استفاده نیست.
|
||||
|
||||
هدف: سه اندپوینت عمومیِ جدید تا سایت بتواند
|
||||
۱) منابعِ قابلِ رزروِ یک پزشک را ببیند، ۲) وقتهای خالیِ یک منبع برای سرویسهای انتخابشده را
|
||||
بگیرد، ۳) روزهای فعالِ ماه را برای تقویم بگیرد.
|
||||
بهعلاوه رفع یک ناسازگاریِ موجود در مسیر عمومیِ ثبت نوبت (وظیفهٔ ۴).
|
||||
|
||||
**گیتِ عمومیشدن دقیقاً همان توگل است:** منبع وقتی در سایت دیده میشود که دستکم یک
|
||||
`ResourceServiceOffering` فعال به یک `ServiceItem` با `bookable = true` و `active = true` داشته باشد.
|
||||
|
||||
**تصمیم دامنه (تأییدشده):** روی صفحهٔ یک پزشک فقط منابعی میآیند که پزشکِ ناظرشان
|
||||
(`supervisor`) همان پزشک است، یا خودشان پلِ همان پزشکاند (`doctor_id`). فهرست کلینیکمحور
|
||||
خارج از این تسک است.
|
||||
|
||||
## معیار پذیرش
|
||||
|
||||
- ✅ موفق:
|
||||
- `GET /api/v1/appointment-booking-resources/{doctorUuid}` بدون هیچ توکنی → ۲۰۰ با
|
||||
`{ success: true, data: { doctor_uuid, clinic_uuid, resources: [...] } }`؛ هر منبع فیلد
|
||||
`services[]` دارد و در آن فقط سرویسهای `bookable=true` و `active=true` با
|
||||
`duration_minutes` و `price_rials`ِ حلشده از `ResourceServiceResolver` هستند.
|
||||
- `GET /api/v1/appointment-resource-slots?resource_uuid=..&date=..&service_item_uuids[]=..`
|
||||
بدون توکن → ۲۰۰ با `start_times[]` که هر عضوش `{start, end, start_time, end_time}` است، و
|
||||
`total_duration_minutes` برابرِ مجموعِ مدتِ حلشدهٔ همان منبع.
|
||||
- `GET /api/v1/appointment-resource-month-availability/{resourceUuid}?year=&month=&service_item_uuids[]=`
|
||||
بدون توکن → ۲۰۰ با `enabled_dates[]` و `disabled_dates[]` که مجموعشان همهٔ روزهای آن ماه است.
|
||||
- `POST /api/v1/appointment` با `resource_uuid` + `service_item_uuids[]` + یکی از
|
||||
`start_times`ِ بالا → ۲۰۱، و `slot_end` دقیقاً برابرِ `end`ِ همان start_time.
|
||||
- ❌ خطا:
|
||||
- منبعی که هیچ سرویسِ `bookable` ندارد → در پاسخِ فهرست **نمیآید**؛ و
|
||||
`appointment-resource-slots` رویش → ۴۲۲ با کد `ERR_VALIDATION_001` و
|
||||
`field: service_item_uuids`.
|
||||
- سرویسی که `bookable=false` است ولی روی منبع offering فعال دارد → `resource-slots` → ۴۲۲
|
||||
(پیام: «این سرویس برای نوبتدهی آنلاین فعال نیست»). یعنی مسیر عمومی سختگیرتر از پنل است.
|
||||
- `date` با فرمت نادرست → ۴۲۲ با `field: date`. `resource_uuid` ناموجود یا `active=false` →
|
||||
۴۲۲ با `field: resource_uuid` (نه ۴۰۴؛ همان الگوی موجود در `AppointmentController`).
|
||||
- `POST /api/v1/appointment` روی بازهای که منبع در آن پر است → ۴۰۹ با
|
||||
`field: resource_uuid` (رفتار موجودِ `ResourceOccupier`؛ نباید بشکند).
|
||||
- ⚠️ مرزی:
|
||||
- پزشکی که هیچ منبعی ندارد → ۲۰۰ با `resources: []`، نه ۴۰۴.
|
||||
- منبعِ با `capacity > 1`: وقتی یک نوبت روی آن نشسته، همان بازه هنوز باید در `start_times` بیاید
|
||||
(ظرفیت هنوز پر نشده). `ResourceBookingSlotService::freeIntervals()` این را میداند؛ فقط با
|
||||
داده تست شود.
|
||||
- روزی که منبع شیفت ندارد → `start_times: []` و همان روز در `disabled_dates` ماه.
|
||||
- `service_item_uuids` خالی → ۴۲۲ با پیام «انتخاب حداقل یک سرویس الزامی است» (رفتار موجودِ
|
||||
`resolveDuration`).
|
||||
- آخرین روز ماه شمسی/میلادی: `month-availability` بر پایهٔ سال/ماهِ **میلادی** است — همان
|
||||
قرارداد `appointment-settings/month-availability` که سایت از قبل با آن کار میکند. تغییرش نده.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `src/Resource/Entity/ClinicResource.php` | منبع؛ `address`, `type`, `supervisor`, `doctor`, `capacity`, `active` |
|
||||
| `src/Resource/Entity/ResourceServiceOffering.php` | جفتِ منبع↔سرویس با مدت/قیمت/`active` |
|
||||
| `src/Resource/Repository/ClinicResourceRepository.php` | کوئریهای منبع؛ متد جدید اینجا |
|
||||
| `src/Resource/Repository/ResourceServiceOfferingRepository.php` | `findForResource`, `bookableResourceIds`, `activeResourceIdsFor` |
|
||||
| `src/Resource/Service/ResourceBookingSlotService.php` | `resolveDuration`, `startTimes`, `freeIntervals`, `assertOffered` |
|
||||
| `src/ClinicService/Service/ResourceServiceResolver.php` | زنجیرهٔ حلِ مدت و قیمت |
|
||||
| `src/Resource/Controller/ResourceBookingSlotController.php` | نسخهٔ پنلیِ همین اسلاتها — الگوی مرجع |
|
||||
| `src/Appointment/Controller/AppointmentController.php` | اندپوینتهای عمومی موجود + `POST /api/v1/appointment` |
|
||||
| `config/packages/security.yaml` | `access_control` |
|
||||
| `docs/api/resource.md`، `docs/api/appointment.md` | مستندات |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
منبعِ حقیقتِ مدت و قیمت، این زنجیره است:
|
||||
|
||||
```php
|
||||
// src/ClinicService/Service/ResourceServiceResolver.php:41
|
||||
public function resolve(
|
||||
ClinicResource $resource,
|
||||
ServiceItem $item,
|
||||
DoctorAddress $address,
|
||||
?ServiceItem $parentService = null,
|
||||
): ResolvedServiceSpec
|
||||
// ۱. منبع+گزینه → ۲. منبع+سرویس → ۳. شعبه → ۴. پیشفرض سرویس
|
||||
```
|
||||
|
||||
و اسلاتها:
|
||||
|
||||
```php
|
||||
// src/Resource/Service/ResourceBookingSlotService.php:87
|
||||
public function startTimes(
|
||||
ClinicResource $resource,
|
||||
string $date,
|
||||
int $totalMinutes,
|
||||
?int $excludeAppointmentId = null,
|
||||
): array
|
||||
```
|
||||
|
||||
مسیر عمومیِ ثبت نوبت، مدت را از calculatorِ پزشکمحور میگیرد — حتی وقتی منبع دارد:
|
||||
|
||||
```php
|
||||
// src/Appointment/Controller/AppointmentController.php:528-538
|
||||
$duration = null;
|
||||
if ($hasServices) {
|
||||
$duration = $this->serviceCalculator->calculate($doctor, $bookingClinic, $serviceUuids);
|
||||
$slotEnd = $duration->endFor($slotStart);
|
||||
}
|
||||
|
||||
if ($resource !== null) {
|
||||
[$bookingType, $bookingId] = EntityContext::forBooking($doctor, $bookingClinic)->toEntityPair();
|
||||
// ...
|
||||
```
|
||||
|
||||
مسیر پنل همین را درست انجام میدهد و باید الگو باشد:
|
||||
|
||||
```php
|
||||
// src/Appointment/Controller/MyAppointmentsController.php:188-197
|
||||
if (!empty($serviceUuids) && !$isReserve && $resource !== null) {
|
||||
// نوبتِ منبع: مدت از زنجیرهٔ حلِ همان منبع میآید (هر دستگاه مدت خودش را
|
||||
// دارد) و گیتِ «این سرویس روی این منبع فعال است؟» جای `bookable` مینشیند.
|
||||
['minutes' => $resourceMinutes, 'items' => $serviceItems] =
|
||||
$this->resourceSlots->resolveDuration($resource, $serviceUuids, $durationOverrides);
|
||||
```
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. کوئری منابعِ قابلِ رزروِ عمومی
|
||||
|
||||
در `src/Resource/Repository/ClinicResourceRepository.php` متد جدید:
|
||||
|
||||
```php
|
||||
/**
|
||||
* منابعِ یک پزشک در یک محیط که در سایت عمومی قابل رزروند.
|
||||
*
|
||||
* سه شرط با هم: منبع فعال، دستکم یک offering فعال، و سرویسِ آن offering هم
|
||||
* `bookable` و هم `active`. توگلِ «نمایش در نوبتدهی آنلاین» تنها گیتِ عمومیشدن است؛
|
||||
* منبعی که سرویسِ روشنی ندارد اصلاً نباید در پاسخ دیده شود.
|
||||
*
|
||||
* @return ClinicResource[]
|
||||
*/
|
||||
public function findPublicBookableForDoctor(string $entityType, int $entityId, Doctor $doctor): array
|
||||
{
|
||||
return $this->createQueryBuilder('r')
|
||||
->join('r.type', 't')
|
||||
->addSelect('t')
|
||||
->join(ResourceServiceOffering::class, 'o', 'WITH', 'o.resource = r')
|
||||
->join('o.serviceItem', 'i')
|
||||
->where('r.entityType = :type')
|
||||
->andWhere('r.entityId = :id')
|
||||
->andWhere('r.active = true')
|
||||
->andWhere('o.active = true')
|
||||
->andWhere('i.bookable = true')
|
||||
->andWhere('i.active = true')
|
||||
// فقط منابع همین پزشک: یا خودش پلِ پزشک است، یا پزشک ناظرش همین است.
|
||||
->andWhere('r.doctor = :doctor OR r.supervisor = :doctor')
|
||||
->setParameter('type', $entityType)
|
||||
->setParameter('id', $entityId)
|
||||
->setParameter('doctor', $doctor)
|
||||
->distinct()
|
||||
->orderBy('r.name', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
```
|
||||
|
||||
**نحوه تست:** یک unit/functional تست که سه سناریو بسازد — منبع با سرویسِ bookable (باید بیاید)،
|
||||
منبع با سرویسِ `bookable=false` (نباید بیاید)، منبع با offering غیرفعال (نباید بیاید) — و منبعی
|
||||
که ناظرش پزشک دیگری است (نباید بیاید).
|
||||
|
||||
### ۲. سرویسِ ساختِ payload عمومی
|
||||
|
||||
فایل جدید `src/Resource/Service/PublicResourceBookingService.php`.
|
||||
کنترلر نازک میماند؛ این کلاس تنها مسئولیتش «منبع → آرایهٔ عمومی» است (SRP).
|
||||
|
||||
```php
|
||||
/**
|
||||
* نمای عمومیِ منبع برای سایت نوبتدهی.
|
||||
*
|
||||
* از نمای پنل جداست چون سؤالِ دیگری جواب میدهد: پنل همهٔ سرویسهای منبع را
|
||||
* میخواهد، سایت فقط آنهایی را که مالک روشن کرده. ادغامشان یعنی یک `if ($public)`
|
||||
* در دلِ کد پنل و یک راهِ تازه برای نشتِ سرویسِ خاموش.
|
||||
*/
|
||||
final class PublicResourceBookingService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClinicResourceRepository $resources,
|
||||
private readonly ResourceServiceOfferingRepository $offerings,
|
||||
private readonly ResourceServiceResolver $resolver,
|
||||
) {}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
public function resourcesFor(Doctor $doctor, ?Clinic $clinic): array;
|
||||
|
||||
/**
|
||||
* سرویسهای عمومیِ یک منبع — همان گیتِ فهرست، تا اسلات و فهرست از هم واگرا نشوند.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function publicServices(ClinicResource $resource): array;
|
||||
|
||||
/** @throws AppException ۴۲۲ روی سرویسی که در سایت روشن نیست */
|
||||
public function assertPublicService(ClinicResource $resource, ServiceItem $item): void;
|
||||
}
|
||||
```
|
||||
|
||||
شکل هر منبع در پاسخ:
|
||||
|
||||
```php
|
||||
[
|
||||
'uuid' => $resource->getUuid(),
|
||||
'name' => $resource->getName(),
|
||||
'type' => ['code' => $type->getCode(), 'name' => $type->getName()],
|
||||
'capacity' => $resource->getCapacity(),
|
||||
'location' => [
|
||||
'uuid' => $address->getUuid(),
|
||||
'title' => $address->getName() ?: 'مطب شخصی',
|
||||
'address' => $address->getAddress(),
|
||||
],
|
||||
'supervisor' => ['uuid' => ..., 'full_name' => ...] | null,
|
||||
'services' => [
|
||||
[
|
||||
'uuid' => $item->getUuid(),
|
||||
'name' => $item->getName(),
|
||||
'duration_minutes' => $spec->durationMinutes, // از resolver، نه از پیشفرض خام
|
||||
'price_rials' => $spec->priceRials,
|
||||
'service_section' => ['uuid' => ..., 'name' => ...],
|
||||
],
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
نکتهٔ کارایی: `publicServices()` نباید به ازای هر سرویس یک `findOneFor` جدا بزند وقتی
|
||||
`findForResource($resource)` همه را یکجا میدهد. offeringهای همان منبع را یکبار بخوان و
|
||||
سرویسهای `bookable && active` را از رویش فیلتر کن؛ `resolver->resolve()` را فقط برای همانها صدا بزن.
|
||||
|
||||
**نحوه تست:** `ddev exec php bin/phpunit` روی یک تستِ سرویس با دو سرویس (یکی روشن، یکی خاموش)
|
||||
و یک offering با `duration_minutes` اختصاصی؛ ادعا: خروجی یک عضو دارد و مدتش عددِ offering است،
|
||||
نه `solo_duration_minutes`ِ سرویس.
|
||||
|
||||
### ۳. کنترلر عمومی + مسیرها
|
||||
|
||||
فایل جدید `src/Resource/Controller/PublicResourceBookingController.php` — `extends BaseController`،
|
||||
**بدون** `IsGranted` و بدون `ResourcePermissionTrait`.
|
||||
|
||||
```php
|
||||
/**
|
||||
* نوبتدهی منبعمحور برای سایت عمومی.
|
||||
*
|
||||
* از {@see ResourceBookingSlotController} جداست و نه یک پرچمِ `public` روی آن: آنجا منبع
|
||||
* از محیطِ کاربرِ احرازشده حل میشود (`ResourceContext::resource($user, $uuid)`) و اینجا
|
||||
* کاربری وجود ندارد. یک کنترلر با دو مدلِ اعتماد، همانجایی است که نشت اتفاق میافتد.
|
||||
*/
|
||||
#[OA\Tag(name: 'Resource')]
|
||||
class PublicResourceBookingController extends BaseController
|
||||
{
|
||||
// GET /api/v1/appointment-booking-resources/{doctorUuid}?clinic_uuid=
|
||||
// GET /api/v1/appointment-resource-slots?resource_uuid=&date=&service_item_uuids[]=
|
||||
// GET /api/v1/appointment-resource-month-availability/{resourceUuid}?year=&month=&service_item_uuids[]=
|
||||
}
|
||||
```
|
||||
|
||||
قواعدی که باید رعایت شوند:
|
||||
|
||||
- محیط با همان الگوی موجود حل شود:
|
||||
`[$type, $id] = EntityContext::forBooking($doctor, $clinic)->toEntityPair();`
|
||||
و `$clinic` از `AppointmentController::bookingClinic()` — اگر متد `private` است، منطقش را
|
||||
کپی نکن؛ یا در یک سرویس مشترک بگذار یا از `BookingContextResolver` استفاده کن. تصمیم و دلیلش
|
||||
را در کامنت بنویس.
|
||||
- در `resource-slots` و `month-availability` منبع با `ClinicResourceRepository::findByUuid()`
|
||||
گرفته میشود و **باید** بررسی شود: `isActive()` و اینکه دستکم یک سرویس عمومی دارد. منبعِ
|
||||
ناموجود یا خاموش → ۴۲۲ با `field: resource_uuid`.
|
||||
- برای هر uuid در `service_item_uuids` اول `assertPublicService()` صدا زده شود (گیتِ `bookable`)،
|
||||
بعد `ResourceBookingSlotService::resolveDuration()` (گیتِ offering + مدت). ترتیب مهم است:
|
||||
پیامِ «در سایت فعال نیست» گویاتر از «این منبع این سرویس را ارائه نمیدهد» است.
|
||||
- `durations[]` که نسخهٔ پنلی میپذیرد **در مسیر عمومی پذیرفته نشود** — override مدت ابزار منشی
|
||||
است؛ در دست بازدیدکننده یعنی ساختن ظرفیتِ جعلی. یعنی `resolveDuration($resource, $uuids)`
|
||||
بدون آرگومان سوم.
|
||||
- `month-availability` روی روزهای ماه حلقه بزند و برای هر روز `startTimes(...) !== []` را
|
||||
بسنجد — همان الگوی `AppointmentController::monthAvailability()` که با `hasAnyAvailability`
|
||||
کار میکند. اگر روی ۳۱ روز کند بود، بهجای `startTimes` از `freeIntervals` استفاده کن و فقط
|
||||
وجودِ یک بازهٔ بهاندازهٔ کافی بلند را چک کن.
|
||||
|
||||
سپس در `config/packages/security.yaml`، بخش `access_control`، کنار همتاهای موجود:
|
||||
|
||||
```yaml
|
||||
- { path: ^/api/v1/appointment-booking-resources/, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api/v1/appointment-resource-slots, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api/v1/appointment-resource-month-availability/, roles: PUBLIC_ACCESS }
|
||||
```
|
||||
|
||||
مسیرها روی firewallِ `api` میمانند (به `public_endpoints` **اضافه نشوند**) — دقیقاً به همان
|
||||
دلیلی که در کامنت بالای `public_endpoints` نوشته شده: توکن اختیاری بماند.
|
||||
|
||||
**نحوه تست:**
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console debug:router | grep resource
|
||||
# سه مسیر جدید باید دیده شوند
|
||||
|
||||
curl -s "https://clinic-pro.ddev.site/api/v1/appointment-booking-resources/<DOCTOR_UUID>" | jq
|
||||
curl -s "https://clinic-pro.ddev.site/api/v1/appointment-resource-slots?resource_uuid=<R>&date=2026-08-15&service_item_uuids[]=<S>" | jq
|
||||
curl -s "https://clinic-pro.ddev.site/api/v1/appointment-resource-month-availability/<R>?year=2026&month=8&service_item_uuids[]=<S>" | jq
|
||||
```
|
||||
|
||||
هر سه بدون هدر `Authorization` و همه باید ۲۰۰ بدهند. سپس همان `curl`ها را روی سرویسی بزن که
|
||||
`bookable` را در پنل خاموش کردهای و ۴۲۲ بگیر.
|
||||
اکانت تست پنل برای ساختن داده: `09390039833` / `09390039833`.
|
||||
|
||||
### ۴. رفعِ مدتِ نوبتِ منبعدار در مسیر عمومی
|
||||
|
||||
در `src/Appointment/Controller/AppointmentController.php::book()`، شاخهٔ محاسبهٔ مدت باید مثل
|
||||
مسیر پنل، وقتی منبع هست از `ResourceBookingSlotService::resolveDuration()` استفاده کند:
|
||||
|
||||
```php
|
||||
$duration = null;
|
||||
$resourceItems = [];
|
||||
|
||||
if ($hasServices && $resource !== null) {
|
||||
// مدت از زنجیرهٔ حلِ همان منبع میآید، وگرنه `slot_end` با start_timesِ
|
||||
// appointment-resource-slots یکی نمیشود و بیمار وقتی را میگیرد که سرور
|
||||
// جای دیگری آزاد حساب کرده بود.
|
||||
foreach ($serviceUuids as $u) { /* assertPublicService(...) */ }
|
||||
['minutes' => $minutes, 'items' => $resourceItems] =
|
||||
$this->resourceSlots->resolveDuration($resource, $serviceUuids);
|
||||
$slotEnd = $slotStart + $minutes * 60;
|
||||
} elseif ($hasServices) {
|
||||
$duration = $this->serviceCalculator->calculate($doctor, $bookingClinic, $serviceUuids);
|
||||
$slotEnd = $duration->endFor($slotStart);
|
||||
}
|
||||
```
|
||||
|
||||
سه قید:
|
||||
|
||||
- ترازِ سطلِ اشغال (`OccupancyBucket::alignWindow`) در خط ۵۱۵ **قبل از** محاسبهٔ مدت اجرا
|
||||
میشود و در آنجا `$slotEnd` هنوز مقدارِ کلاینت است. بعد از بازنویسیِ `$slotEnd`، تراز باید
|
||||
دوباره اعمال شود؛ وگرنه بازهٔ نهایی ناتراز میماند.
|
||||
- ذخیرهٔ سرویسها روی نوبت (خط ~۶۰۵، `replaceServiceItems` + `setServiceDuration`) نباید بشکند.
|
||||
در شاخهٔ منبع، `$duration` تهی است، پس این بلوک باید `items` و `minutes`ِ منبع را هم بپذیرد.
|
||||
- بررسیِ موجودِ «این منبع این سرویس را ارائه میدهد؟» (خط ۵۵۸ با `hasAnyFor`/`activeResourceIdsFor`)
|
||||
حالا با `resolveDuration` تکراری میشود. یکی را نگه دار — `resolveDuration` سختگیرتر است چون
|
||||
`offering.active` را هم میبیند. حذفِ کدِ تکراری را در همان commit توضیح بده.
|
||||
|
||||
**نحوه تست:** یک تست functional که:
|
||||
۱) از `appointment-resource-slots` یک `start_time` بگیرد،
|
||||
۲) با همان `start` و همان `service_item_uuids` روی `POST /api/v1/appointment` بزند،
|
||||
۳) ادعا کند `slot_end` پاسخ برابر `end`ِ همان start_time است.
|
||||
قبل از این تغییر باید قرمز شود.
|
||||
|
||||
### ۵. مستندات
|
||||
|
||||
- `docs/api/resource.md`: سه اندپوینت عمومی جدید با نمونهٔ درخواست/پاسخ و جدول خطاها.
|
||||
- `docs/api/appointment.md`: رفتار `resource_uuid` در `POST /api/v1/appointment` — که مدت از
|
||||
منبع میآید و در مسیر عمومی سرویس باید `bookable` باشد.
|
||||
- اگر فایل `docs/api/appointment-booking.md` جریان عمومی را توصیف میکند، مرحلهٔ منبعمحور را
|
||||
هم آنجا اضافه کن.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **گیتِ عمومی یک جا تعریف شود.** «سرویس در سایت دیده میشود» = `bookable && active` + offering
|
||||
فعال. این شرط در سه جا لازم است (فهرست، اسلات، ثبت نوبت). یک متد در
|
||||
`PublicResourceBookingService` و صدا زدنش از هر سه — نه سهبار نوشتنِ همان `if`.
|
||||
- **جداسازی محیط:** منبع با uuid از بیرون میآید و `TenantFilter` پوششش نمیدهد
|
||||
(`ResourceServiceOffering` فرزندِ aggregate است). در هر سه اندپوینت، تطابقِ
|
||||
`(entity_type, entity_id)`ِ منبع با محیطِ رزروِ همان پزشک/کلینیک باید صریح بررسی شود — همان
|
||||
کاری که `AppointmentController` در خط ۵۴۹ میکند.
|
||||
- **fail-safe عمومی:** پارامتر `management=1` در این کنترلر معنا ندارد و پیاده نشود. اگر روزی
|
||||
پنل بخواهد همین دید را داشته باشد، مسیر پنلیِ خودش را دارد.
|
||||
- **`durations[]` در مسیر عمومی ممنوع** — دلیلش بالا آمده.
|
||||
- ظرفیت و اشغال را خودت حساب نکن؛ `ResourceBookingSlotService` هر دو منبعِ اشغال را میبیند
|
||||
(`appointments.resource_id` و `resource_occupancy`). دور زدنش یعنی نوبتِ نامرئی.
|
||||
- تاریخها Unix timestamp صحیحاند؛ `start_time`/`end_time` رشتهٔ `H:i` به وقتِ محلیِ شعبهٔ منبع
|
||||
است (`ResourceBookingSlotService::dayStart()` این را از `address->getTimezone()` میگیرد).
|
||||
- **مصرفکنندهٔ cross-repo:** این قرارداد را `nobat724_front/services/response.js` مصرف میکند.
|
||||
هر تغییر در نام فیلدها بعد از این، در build سایت خطا **نمیدهد** — پس شکل پاسخ را قبل از
|
||||
merge نهایی کن.
|
||||
Reference in New Issue
Block a user