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
|
provider: api_doc_provider
|
||||||
|
|
||||||
public_endpoints:
|
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
|
stateless: true
|
||||||
security: false
|
security: false
|
||||||
|
|
||||||
@@ -60,6 +60,8 @@ security:
|
|||||||
- { path: ^/api/v1/user/otp-login, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/user/otp-login, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/user/reset-password, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/user/reset-password, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/appointment-slots, 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/appointment-settings/month-availability/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/comments/, roles: PUBLIC_ACCESS }
|
- { path: ^/api/v1/comments/, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/v1/site-context$, methods: [GET], 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
|
### Errors
|
||||||
| Code | HTTP | Description |
|
| 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}`
|
## 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.
|
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'])]
|
#[Route('/api/v1/appointment-settings/month-availability/{doctorUuid}', methods: ['GET'])]
|
||||||
public function monthAvailability(string $doctorUuid, Request $request): JsonResponse
|
public function monthAvailability(string $doctorUuid, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,6 +76,27 @@ class ServiceItemRepository extends ServiceEntityRepository
|
|||||||
->getSingleScalarResult();
|
->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
|
public function save(ServiceItem $item): void
|
||||||
{
|
{
|
||||||
$this->getEntityManager()->persist($item);
|
$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