feat(migrations): add clinic_id context to weekly_schedules, date_overrides, and holidays
- Introduced clinic_id to weekly_schedules, date_overrides, and holidays to differentiate between personal and clinic schedules. - Updated unique constraints and indexes to accommodate the new clinic context. feat(command): create AssignScheduleClinicCommand to move schedules - Added a command to move a doctor's personal weekly schedule into a clinic context. - Implemented checks to ensure sessions align with the target clinic. feat(context): implement EntityContext and EntityContextResolver - Created EntityContext to represent the effective working environment of a request (doctor or clinic). - Developed EntityContextResolver to determine the execution context based on user roles and active contexts. test: add ServiceModeContextTest for appointment scheduling - Implemented tests to ensure service booking respects clinic and personal contexts. - Verified that financial data is omitted in clinic contexts in InvitedDoctorDashboardScopeTest.
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Appointment\Repository\DateOverrideRepository;
|
||||
use App\Appointment\Repository\HolidayRepository;
|
||||
use App\Appointment\Repository\WeeklyScheduleRepository;
|
||||
use App\Appointment\Entity\WeeklySchedule;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
|
||||
|
||||
@@ -25,9 +26,9 @@ class SlotCalculatorService
|
||||
*
|
||||
* @return array[] [{start, end, start_time, end_time, location_id}]
|
||||
*/
|
||||
public function getAvailableSlots(Doctor $doctor, string $date): array
|
||||
public function getAvailableSlots(Doctor $doctor, string $date, ?Clinic $clinic = null): array
|
||||
{
|
||||
$sessions = $this->buildAllSessions($doctor, $date);
|
||||
$sessions = $this->buildAllSessions($doctor, $date, $clinic);
|
||||
if (empty($sessions)) return [];
|
||||
$flat = array_merge(...array_map(fn($s) => $s['slots'], $sessions));
|
||||
return $this->filterBookedSlots($doctor, $flat);
|
||||
@@ -36,10 +37,10 @@ class SlotCalculatorService
|
||||
/**
|
||||
* آدرس (location_id) متناظر با اسلاتِ شروعشده در تاریخ مشخص. اگر پیدا نشد null.
|
||||
*/
|
||||
public function resolveSlotLocationId(Doctor $doctor, int $slotStart): ?int
|
||||
public function resolveSlotLocationId(Doctor $doctor, int $slotStart, ?Clinic $clinic = null): ?int
|
||||
{
|
||||
$date = date('Y-m-d', $slotStart);
|
||||
$sessions = $this->buildAllSessions($doctor, $date);
|
||||
$sessions = $this->buildAllSessions($doctor, $date, $clinic);
|
||||
foreach ($sessions as $session) {
|
||||
foreach (($session['slots'] ?? []) as $slot) {
|
||||
if ((int) ($slot['start'] ?? 0) === $slotStart) {
|
||||
@@ -57,9 +58,9 @@ class SlotCalculatorService
|
||||
*
|
||||
* @return array[] [{start_time, end_time, slots: [{start, end, start_time, end_time, location_id, is_available}]}]
|
||||
*/
|
||||
public function getAllSlotsWithAvailability(Doctor $doctor, string $date): array
|
||||
public function getAllSlotsWithAvailability(Doctor $doctor, string $date, ?Clinic $clinic = null): array
|
||||
{
|
||||
$sessions = $this->buildAllSessions($doctor, $date);
|
||||
$sessions = $this->buildAllSessions($doctor, $date, $clinic);
|
||||
$now = time();
|
||||
return array_map(fn(array $session) => [
|
||||
'start_time' => $session['start_time'],
|
||||
@@ -75,9 +76,9 @@ class SlotCalculatorService
|
||||
* Whether a doctor has at least one slot on the given date.
|
||||
* Lightweight check for the month-availability endpoint.
|
||||
*/
|
||||
public function hasAnyAvailability(Doctor $doctor, string $date): bool
|
||||
public function hasAnyAvailability(Doctor $doctor, string $date, ?Clinic $clinic = null): bool
|
||||
{
|
||||
return !empty($this->buildAllSessions($doctor, $date));
|
||||
return !empty($this->buildAllSessions($doctor, $date, $clinic));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,15 +93,15 @@ class SlotCalculatorService
|
||||
*
|
||||
* @return array<array{start:int,end:int,start_time:string,end_time:string,location_id:?int}>
|
||||
*/
|
||||
public function getServiceStartTimes(Doctor $doctor, string $date, int $durationMinutes): array
|
||||
public function getServiceStartTimes(Doctor $doctor, string $date, int $durationMinutes, ?Clinic $clinic = null): array
|
||||
{
|
||||
if ($durationMinutes <= 0) return [];
|
||||
|
||||
$buffer = (int)($this->getBookingMeta($doctor)['buffer_minutes'] ?? 0);
|
||||
$buffer = (int)($this->getBookingMeta($doctor, $clinic)['buffer_minutes'] ?? 0);
|
||||
$durSec = $durationMinutes * 60;
|
||||
$needSec = $durSec + $buffer * 60; // فضای لازم شامل بافر
|
||||
|
||||
$sessions = $this->buildAllSessions($doctor, $date); // window/holiday/override/booking-window رعایت میشود
|
||||
$sessions = $this->buildAllSessions($doctor, $date, $clinic); // window/holiday/override/booking-window رعایت میشود
|
||||
if (empty($sessions)) return [];
|
||||
|
||||
$dayStart = (int) strtotime($date . ' 00:00:00');
|
||||
@@ -152,14 +153,14 @@ class SlotCalculatorService
|
||||
* Booking is allowed only when online booking is enabled and the date is
|
||||
* today..(today + window). Past dates are always rejected.
|
||||
*/
|
||||
private function isWithinBookingWindow(Doctor $doctor, int $dayStart): bool
|
||||
private function isWithinBookingWindow(Doctor $doctor, int $dayStart, ?Clinic $clinic): bool
|
||||
{
|
||||
$todayStart = (int) strtotime('today 00:00:00');
|
||||
if ($dayStart < $todayStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$meta = $this->getBookingMeta($doctor);
|
||||
$meta = $this->getBookingMeta($doctor, $clinic);
|
||||
if (!($meta['online_booking_enabled'] ?? true)) {
|
||||
return false;
|
||||
}
|
||||
@@ -171,9 +172,9 @@ class SlotCalculatorService
|
||||
return $dayStart <= $maxStart;
|
||||
}
|
||||
|
||||
private function getBookingMeta(Doctor $doctor): array
|
||||
private function getBookingMeta(Doctor $doctor, ?Clinic $clinic): array
|
||||
{
|
||||
$schedule = $this->scheduleRepo->findByDoctor($doctor);
|
||||
$schedule = $this->scheduleRepo->findByDoctorAndClinic($doctor, $clinic);
|
||||
return $schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META;
|
||||
}
|
||||
|
||||
@@ -182,23 +183,23 @@ class SlotCalculatorService
|
||||
*
|
||||
* @return array[] [{start_time: string, end_time: string, slots: array[]}]
|
||||
*/
|
||||
private function buildAllSessions(Doctor $doctor, string $date): array
|
||||
private function buildAllSessions(Doctor $doctor, string $date, ?Clinic $clinic = null): array
|
||||
{
|
||||
$dayStart = (int) strtotime($date . ' 00:00:00');
|
||||
$dayEnd = $dayStart + 86400;
|
||||
|
||||
// 0. Online booking disabled or date outside the booking window
|
||||
if (!$this->isWithinBookingWindow($doctor, $dayStart)) {
|
||||
if (!$this->isWithinBookingWindow($doctor, $dayStart, $clinic)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 1. Blocked by holiday
|
||||
if (!empty($this->holidayRepo->findActiveByDoctor($doctor, $dayStart, $dayEnd - 1))) {
|
||||
if (!empty($this->holidayRepo->findActiveByDoctor($doctor, $dayStart, $dayEnd - 1, $clinic))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. Date override takes precedence over weekly schedule
|
||||
foreach ($this->overrideRepo->findByDoctor($doctor) as $override) {
|
||||
foreach ($this->overrideRepo->findByDoctorAndClinic($doctor, $clinic) as $override) {
|
||||
if (date('Y-m-d', $override->getDate()) === $date) {
|
||||
if (!$override->isActive()) return [];
|
||||
return $this->buildSessionsFromOverride($override->getSetting() ?? [], $dayStart);
|
||||
@@ -206,7 +207,7 @@ class SlotCalculatorService
|
||||
}
|
||||
|
||||
// 3. Weekly schedule
|
||||
$schedule = $this->scheduleRepo->findByDoctor($doctor);
|
||||
$schedule = $this->scheduleRepo->findByDoctorAndClinic($doctor, $clinic);
|
||||
if ($schedule === null) return [];
|
||||
|
||||
// Convert PHP date('w') (0=Sunday) to Iranian week index (0=Saturday)
|
||||
|
||||
Reference in New Issue
Block a user