diff --git a/config/packages/security.yaml b/config/packages/security.yaml index e8c3bf92..26f233d5 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -33,7 +33,7 @@ security: provider: api_doc_provider public_endpoints: - pattern: ^/(api/v1/altcha/(challenge|config)$|api/v1/user/(send-code|verify-code|register|otp-login|reset-password)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic/[^/]+/addresses$|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/appointment-settings/month-availability/|api/v1/comments/|api/v1/rate/[^/]+$|api/v1/specialties|api/v1/blogs$|api/v1/tags$|api/v1/clinic-invitation/|api/v1/pre-registration$|api/v1/doctor/[^/]+/claim-info$) + pattern: ^/(api/v1/altcha/(challenge|config)$|api/v1/user/(send-code|verify-code|register|otp-login|reset-password)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic/[^/]+/addresses$|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/appointment-service-slots|api/v1/appointment-booking-services/|api/v1/appointment-settings/month-availability/|api/v1/comments/|api/v1/rate/[^/]+$|api/v1/specialties|api/v1/blogs$|api/v1/tags$|api/v1/clinic-invitation/|api/v1/pre-registration$|api/v1/doctor/[^/]+/claim-info$) stateless: true security: false @@ -60,6 +60,8 @@ security: - { path: ^/api/v1/user/otp-login, roles: PUBLIC_ACCESS } - { path: ^/api/v1/user/reset-password, roles: PUBLIC_ACCESS } - { path: ^/api/v1/appointment-slots, roles: PUBLIC_ACCESS } + - { path: ^/api/v1/appointment-service-slots, roles: PUBLIC_ACCESS } + - { path: ^/api/v1/appointment-booking-services/, roles: PUBLIC_ACCESS } - { path: ^/api/v1/appointment-settings/month-availability/, roles: PUBLIC_ACCESS } - { path: ^/api/v1/comments/, roles: PUBLIC_ACCESS } - { path: ^/api/v1/site-context$, methods: [GET], roles: PUBLIC_ACCESS } diff --git a/docs/api/appointment.md b/docs/api/appointment.md index 230cf0af..9abd2709 100644 --- a/docs/api/appointment.md +++ b/docs/api/appointment.md @@ -94,7 +94,7 @@ Get all appointment slots (available and booked) for a doctor on a specific date } } ``` -`start_times` خالی یعنی در آن روز فضای کافی نیست. `end` بدونِ بافر است (بافر فقط فاصلهٔ بین نوبت‌های پیشنهادی است). +`start_times` خالی یعنی در آن روز فضای کافی نیست. `end` بدونِ بافر است (بافر فقط فاصلهٔ بین نوبت‌های پیشنهادی است). **عمومی** (بدون احراز هویت — مصرف‌کننده: سایت nobat724). ### Errors | Code | HTTP | Description | @@ -104,6 +104,33 @@ Get all appointment slots (available and booked) for a doctor on a specific date --- +## GET `/api/v1/appointment-booking-services/{doctorUuid}` + +**عمومی.** روش نوبت‌دهی پزشک + سرویس‌های قابل‌انتخاب برای نوبت‌گیری سرویسی. سایت با این پاسخ تصمیم می‌گیرد مرحلهٔ «انتخاب سرویس» را نشان دهد (حالت `service`) یا جریان اسلاتیِ فعلی (حالت `slot`). + +### Response `200` +```json +{ + "success": true, + "data": { + "doctor_uuid": "…", + "booking_mode": "service", + "buffer_minutes": 5, + "services": [ + { "uuid": "…", "name": "عصب‌کشی", "duration_minutes": 30, "price_rials": 5000000 } + ] + } +} +``` +`services` فقط سرویس‌های `bookable=true` و فعالِ پزشک را دارد؛ در حالت `slot` معمولاً خالی است. + +### Errors +| Code | HTTP | Description | +|------|------|-------------| +| `ERR_VALIDATION_002` | 404 | Doctor not found | + +--- + ## GET `/api/v1/appointment-settings/month-availability/{doctorUuid}` Which days of a month are bookable — used by the public calendar to grey out unavailable days. diff --git a/src/Appointment/Controller/AppointmentController.php b/src/Appointment/Controller/AppointmentController.php index 03248c14..c2330255 100644 --- a/src/Appointment/Controller/AppointmentController.php +++ b/src/Appointment/Controller/AppointmentController.php @@ -193,6 +193,38 @@ class AppointmentController extends BaseController ]); } + /** + * عمومی: روش نوبت‌دهی پزشک + سرویس‌های قابل‌انتخاب برای نوبت‌گیری سرویسی. + * سایت با این پاسخ تصمیم می‌گیرد مرحلهٔ انتخاب سرویس را نشان دهد یا جریان اسلاتی. + * + * GET /api/v1/appointment-booking-services/{doctorUuid} + */ + #[Route('/api/v1/appointment-booking-services/{doctorUuid}', methods: ['GET'])] + public function bookingServices(string $doctorUuid): JsonResponse + { + $doctor = $this->doctorRepo->findByUuid($doctorUuid); + if ($doctor === null) { + return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); + } + + $schedule = $this->scheduleRepo->findByDoctor($doctor); + $meta = $schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META; + + $services = array_map(fn(\App\ClinicService\Entity\ServiceItem $i) => [ + 'uuid' => $i->getUuid(), + 'name' => $i->getName(), + 'duration_minutes' => $i->getDurationMinutes(), + 'price_rials' => $i->getPriceRials(), + ], $this->itemRepo->findBookableByEntity('doctor', $doctor->getId())); + + return $this->success([ + 'doctor_uuid' => $doctorUuid, + 'booking_mode' => $meta['booking_mode'], + 'buffer_minutes' => (int) $meta['buffer_minutes'], + 'services' => $services, + ]); + } + #[Route('/api/v1/appointment-settings/month-availability/{doctorUuid}', methods: ['GET'])] public function monthAvailability(string $doctorUuid, Request $request): JsonResponse { diff --git a/src/ClinicService/Repository/ServiceItemRepository.php b/src/ClinicService/Repository/ServiceItemRepository.php index d9af5b57..772df1a2 100644 --- a/src/ClinicService/Repository/ServiceItemRepository.php +++ b/src/ClinicService/Repository/ServiceItemRepository.php @@ -76,6 +76,27 @@ class ServiceItemRepository extends ServiceEntityRepository ->getSingleScalarResult(); } + /** + * سرویس‌های فعالِ «نمایش در نوبت‌دهی» (bookable) یک entity — برای نمایش عمومیِ + * انتخاب سرویس در نوبت‌گیری آنلاین. + * + * @return ServiceItem[] + */ + public function findBookableByEntity(string $entityType, int $entityId): array + { + return $this->createQueryBuilder('i') + ->join('i.section', 's') + ->where('s.entityType = :type') + ->andWhere('s.entityId = :id') + ->andWhere('i.bookable = true') + ->andWhere('i.active = true') + ->setParameter('type', $entityType) + ->setParameter('id', $entityId) + ->orderBy('i.name', 'ASC') + ->getQuery() + ->getResult(); + } + public function save(ServiceItem $item): void { $this->getEntityManager()->persist($item); diff --git a/tests/Appointment/BookingServicesPublicTest.php b/tests/Appointment/BookingServicesPublicTest.php new file mode 100644 index 00000000..dcc06438 --- /dev/null +++ b/tests/Appointment/BookingServicesPublicTest.php @@ -0,0 +1,65 @@ +createUser(['ROLE_DOCTOR']); + $doctor = new Doctor($owner, 'دکتر سرویس'); + $this->em->persist($doctor); + $this->em->flush(); + + $section = new ServiceSection('doctor', $doctor->getId(), 'بخش'); + $this->em->persist($section); + + $bookable = (new ServiceItem($section, 'عصب‌کشی', 500000))->setDurationMinutes(30)->setBookable(true); + $hidden = (new ServiceItem($section, 'معاینه داخلی', 100000))->setDurationMinutes(15)->setBookable(false); + $this->em->persist($bookable); + $this->em->persist($hidden); + + $schedule = new WeeklySchedule($doctor, []); + $schedule->setMeta(['booking_mode' => 'service', 'buffer_minutes' => 5]); + $this->em->persist($schedule); + $this->em->flush(); + + // بدون هدر Authorization + $this->client->request('GET', '/api/v1/appointment-booking-services/' . $doctor->getUuid()); + $this->assertSame(200, $this->responseCode()); + + $body = json_decode($this->client->getResponse()->getContent(), true); + $data = $body['data'] ?? []; + $this->assertSame('service', $data['booking_mode']); + $this->assertSame(5, $data['buffer_minutes']); + // فقط سرویس bookable برمی‌گردد + $this->assertCount(1, $data['services']); + $this->assertSame('عصب‌کشی', $data['services'][0]['name']); + $this->assertSame(30, $data['services'][0]['duration_minutes']); + } + + public function testSlotModeReturnsEmptyServices(): void + { + $owner = $this->createUser(['ROLE_DOCTOR']); + $doctor = new Doctor($owner, 'دکتر اسلاتی'); + $this->em->persist($doctor); + $this->em->flush(); + + $this->client->request('GET', '/api/v1/appointment-booking-services/' . $doctor->getUuid()); + $this->assertSame(200, $this->responseCode()); + + $data = json_decode($this->client->getResponse()->getContent(), true)['data']; + $this->assertSame('slot', $data['booking_mode']); + $this->assertSame([], $data['services']); + } +}