isConfirmed()) { throw new AppException(ErrorCodes::ERR_SLOT_TAKEN, 'این رزرو قبلاً ثبت شده است', 409); } if ($hold->isExpired($now)) { throw new AppException(ErrorCodes::ERR_HOLD_EXPIRED, 'مهلت رزرو موقت تمام شده است', 409); } $occupancies = $this->holds->occupanciesOfHold($hold); if ($occupancies === []) { throw new AppException(ErrorCodes::ERR_HOLD_EXPIRED, 'رزرو موقت دیگر معتبر نیست', 409); } foreach ($occupancies as $occupancy) { $occupancy->markBooked()->setAppointmentId($appointment->getId()); } $this->writeSegments($hold, $appointment); $hold->markConfirmed($now); $this->em->flush(); // مصرف اعتبار **اینجا**ست نه در پیش‌نمایش قیمت: تنها لحظه‌ای که نوبت واقعاً // وجود دارد. کلید یکتای دفتر هم تضمین می‌کند اجرای دوباره جلسهٔ دوم نخورد. $this->packages->consumeFor($appointment); return $appointment; } /** * بخش‌های نوبت از همان `payload` رزرو ساخته می‌شوند، نه از الگوی امروزِ سرویس: * الگو ممکن است بین رزرو و ثبت عوض شده باشد و نوبت باید همان چیزی بماند که کاربر * دیده و پذیرفته. */ private function writeSegments(AppointmentHold $hold, Appointment $appointment): void { $segments = $hold->getPayload()['plan']['segments'] ?? []; foreach ($segments as $segment) { $start = $hold->getStartsAt() + (int) ($segment['offset_minutes'] ?? 0) * 60; $end = $start + (int) ($segment['duration_minutes'] ?? 0) * 60; if ($end <= $start) { continue; } $this->em->persist(new AppointmentSegment( $appointment, (int) ($segment['sequence'] ?? 1), (string) ($segment['name'] ?? '—'), $start, $end, (bool) ($segment['patient_present'] ?? true), )); } } /** * لغو: ردیف‌های اشغال `released` می‌شوند، **حذف فیزیکی نمی‌شوند**. * تاریخچه ورودی گزارش بهره‌وری است. */ public function cancel(Appointment $appointment): int { $occupancies = $this->em->getRepository(ResourceOccupancy::class) ->findBy(['appointmentId' => $appointment->getId()]); $this->holds->release($occupancies); // ردیف `consume` **حذف نمی‌شود**؛ بازگشت یک ردیف تازه است تا تاریخچه بماند. $this->credits->refund($appointment); return count($occupancies); } /** رزروِ منقضی: همان آزادسازی، ولی از سمت رزرو موقت. */ public function releaseHold(AppointmentHold $hold): int { $occupancies = $this->holds->occupanciesOfHold($hold); $this->holds->release($occupancies); return count($occupancies); } }