em->wrapInTransaction(function () use ($course, $address, $doctor, $operator, $now): array { $planned = $course->plannedSessions(); usort($planned, static fn (CourseSession $a, CourseSession $b): int => $a->getSessionNumber() <=> $b->getSessionNumber()); $anchor = $course->lastCompletedAt() ?? $now; $minDays = $this->scheduler->effectiveMinDays($course, $now); $horizon = $now + CourseScheduler::SEARCH_HORIZON_DAYS * 86400; $booked = 0; $skipped = 0; foreach ($planned as $session) { $min = max($anchor + $minDays * 86400, $now); $ideal = $anchor + max($course->getIdealDays(), $minDays) * 86400; $max = $anchor + max($course->getMaxDays(), $minDays) * 86400; if ($min > $horizon) { $skipped++; continue; } $slot = $this->scheduler->slotsFor($course, $address, $min, min($max, $horizon), $ideal, $now)[0] ?? null; if ($slot === null) { throw new AppException( ErrorCodes::ERR_VALIDATION_001, sprintf('برای جلسهٔ %d هیچ وقت مناسبی در بازهٔ مجاز پیدا نشد', $session->getSessionNumber()), 422, 'session_number', ); } $this->bookOne($course, $session, $slot, $address, $doctor, $operator, $now); $booked++; $anchor = $slot->start; } return [ 'booked' => $booked, 'remaining' => $skipped, 'message' => $skipped === 0 ? null : sprintf( '%d جلسه بیرون از بازهٔ %d روزهٔ رزرو افتاد و برنامه‌ریزی‌شده ماند؛ نزدیک‌تر که شدیم رزروشان کنید.', $skipped, CourseScheduler::SEARCH_HORIZON_DAYS, ), ]; }); } private function bookOne( TreatmentCourse $course, CourseSession $session, AvailableSlot $slot, DoctorAddress $address, Doctor $doctor, User $operator, int $now, ): void { $plan = $this->planner->build($course->getServiceItem(), [], $address); $hold = $this->holds->hold( $operator, $plan, $this->assignmentOf($slot), $slot->start, $course->getEntityType(), $course->getEntityId(), $now, ); $appointment = new Appointment($doctor, $course->getPatientRecord()->getUser(), $slot->start, $slot->end); $appointment->assignTenantPair($course->getEntityType(), $course->getEntityId()); $appointment->setServiceItem($course->getServiceItem()); $appointment->setAddressId($address->getId()); $this->em->persist($appointment); $this->em->flush(); $this->booking->confirm($hold, $appointment, $now); $this->linker->link($session, $appointment); } /** @return array> */ private function assignmentOf(AvailableSlot $slot): array { return $slot->assignment->byRole; } }