From f443beb61508a25b1254055ec48953791a0194e8 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Mon, 15 Jun 2026 16:23:12 +0330 Subject: [PATCH] feat(appointment): public month-availability endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add GET /api/v1/appointment-settings/month-availability/{doctorUuid} ?year=&month= (Gregorian) returning disabled_dates / enabled_dates for the month plus the doctor's online_booking flag and window. Lives in AppointmentController (per-method guards) so it is genuinely public — the class-level IsGranted on AppointmentSettingsController would have forced auth. Whitelisted in security.yaml (firewall + access_control). Uses SlotCalculator::hasAnyAvailability per day, so holidays, closed overrides, non-working days and out-of-window dates all come back disabled. Co-Authored-By: Claude Opus 4.8 --- config/packages/security.yaml | 3 +- .../Controller/AppointmentController.php | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 5455770c..567d3d6a 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/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/comments/|api/v1/rate/|api/v1/blogs$|api/v1/clinic-invitation/|api/v1/pre-registration$) + pattern: ^/(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/blogs$|api/v1/clinic-invitation/|api/v1/pre-registration$) stateless: true security: false @@ -60,6 +60,7 @@ 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-settings/month-availability/, roles: PUBLIC_ACCESS } - { path: ^/api/v1/comments/, roles: PUBLIC_ACCESS } - { path: ^/api/v1/rate/, roles: PUBLIC_ACCESS } - { path: ^/api/v1/blogs$, roles: PUBLIC_ACCESS } diff --git a/src/Appointment/Controller/AppointmentController.php b/src/Appointment/Controller/AppointmentController.php index 58bbe8ce..e9222475 100644 --- a/src/Appointment/Controller/AppointmentController.php +++ b/src/Appointment/Controller/AppointmentController.php @@ -3,7 +3,9 @@ namespace App\Appointment\Controller; use App\Appointment\Entity\Appointment; +use App\Appointment\Entity\WeeklySchedule; use App\Appointment\Repository\AppointmentRepository; +use App\Appointment\Repository\WeeklyScheduleRepository; use App\Appointment\Service\SlotCalculatorService; use App\Auth\Entity\User; use App\Doctor\Repository\DoctorRepository; @@ -26,6 +28,7 @@ class AppointmentController extends BaseController private readonly DoctorRepository $doctorRepo, private readonly SlotCalculatorService $slotCalculator, private readonly PatientService $patientService, + private readonly WeeklyScheduleRepository $scheduleRepo, ) {} // ── Public: available slots ─────────────────────────────────────────────── @@ -126,6 +129,49 @@ class AppointmentController extends BaseController ]); } + #[Route('/api/v1/appointment-settings/month-availability/{doctorUuid}', methods: ['GET'])] + public function monthAvailability(string $doctorUuid, Request $request): JsonResponse + { + $doctor = $this->doctorRepo->findByUuid($doctorUuid); + if ($doctor === null) { + return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); + } + + $year = (int) $request->query->get('year'); + $month = (int) $request->query->get('month'); + if ($year < 1970 || $month < 1 || $month > 12) { + return $this->error(ErrorCodes::ERR_VALIDATION_001, 'سال یا ماه نامعتبر است', 422, 'month'); + } + + $daysInMonth = (int) date('t', (int) strtotime(sprintf('%04d-%02d-01', $year, $month))); + + $disabled = []; + $enabled = []; + for ($day = 1; $day <= $daysInMonth; $day++) { + $date = sprintf('%04d-%02d-%02d', $year, $month, $day); + if ($this->slotCalculator->hasAnyAvailability($doctor, $date)) { + $enabled[] = $date; + } else { + $disabled[] = $date; + } + } + + $schedule = $this->scheduleRepo->findByDoctor($doctor); + $meta = $schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META; + + return $this->success([ + 'year' => $year, + 'month' => $month, + 'disabled_dates' => $disabled, + 'enabled_dates' => $enabled, + 'online_booking_enabled' => (bool) $meta['online_booking_enabled'], + 'booking_window' => [ + 'value' => (int) $meta['booking_window_value'], + 'unit' => $meta['booking_window_unit'], + ], + ]); + } + // ── Authenticated: book / manage ───────────────────────────────────────── #[OA\Post(