feat(appointment): add booking services endpoint and update security configuration for public access

This commit is contained in:
hamed
2026-07-15 23:48:38 +03:30
parent 2df16c6d29
commit ac4a430564
5 changed files with 149 additions and 2 deletions
+3 -1
View File
@@ -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 }
+28 -1
View File
@@ -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.
@@ -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
{
@@ -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);
@@ -0,0 +1,65 @@
<?php
namespace App\Tests\Appointment;
use App\Appointment\Entity\WeeklySchedule;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\Doctor;
use App\Tests\ApiTestCase;
/**
* عمومی: GET /api/v1/appointment-booking-services/{doctorUuid} روش نوبت‌دهی و
* سرویس‌های bookable را بدون احراز هویت برمی‌گرداند (نوبت‌گیری آنلاین سرویسی).
*/
class BookingServicesPublicTest extends ApiTestCase
{
public function testReturnsModeAndBookableServicesWithoutAuth(): void
{
$owner = $this->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']);
}
}