feat(appointment): add booking services endpoint and update security configuration for public access
This commit is contained in:
@@ -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
@@ -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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user