getClinic(); return $clinic !== null ? ['clinic', (int) $clinic->getId()] : ['doctor', (int) $appointment->getDoctor()->getId()]; } /** * نوع خدمتِ مؤثر برای محاسبه: انتخابِ نوبت، وگرنه تنها نوع فعالِ tenant، * وگرنه سرپایی (رفتار تاریخیِ ویزیت). */ public function effectiveCategory(Appointment $appointment): ServiceCategory { if ($appointment->getInsuranceServiceCategory() !== null) { return $appointment->getInsuranceServiceCategory(); } [$entityType, $entityId] = $this->tenantOf($appointment); return $this->serviceCategories->defaultCategory($entityType, $entityId) ?? ServiceCategory::Outpatient; } /** * انتخاب بیمهٔ نوبت را از بدنهٔ درخواست اعمال می‌کند. کلیدهای نیامده دست‌نخورده * می‌مانند؛ رشتهٔ خالی یا null یعنی پاک‌کردن انتخاب. * * @param array $data * @throws AppException ۴۲۲ برای نوع خدمتِ نامعتبر/غیرفعال یا بیمهٔ بدون قرارداد فعال */ public function apply(Appointment $appointment, array $data): void { [$entityType, $entityId] = $this->tenantOf($appointment); if (array_key_exists('insurance_service_category', $data)) { $appointment->setInsuranceServiceCategory( $this->resolveCategory($data['insurance_service_category'], $entityType, $entityId) ); } if (array_key_exists('insurance_base_id', $data)) { $appointment->setInsuranceBaseId( $this->resolveInsuranceId($data['insurance_base_id'], $entityType, $entityId, false) ); } if (array_key_exists('insurance_supplementary_id', $data)) { $appointment->setInsuranceSupplementaryId( $this->resolveInsuranceId($data['insurance_supplementary_id'], $entityType, $entityId, true) ); } } private function resolveCategory(mixed $raw, string $entityType, int $entityId): ?ServiceCategory { if ($raw === null || $raw === '') { return null; } $category = ServiceCategory::tryFromValue((string) $raw); if ($category === null) { throw new AppException( ErrorCodes::ERR_VALIDATION_001, 'نوع خدمت نامعتبر است: ' . implode('، ', ServiceCategory::values()), 422, 'insurance_service_category', ); } if (!$this->serviceCategories->isEnabled($entityType, $entityId, $category)) { throw new AppException( ErrorCodes::ERR_VALIDATION_001, sprintf('«%s» در تنظیمات بیمه فعال نیست', $category->label()), 422, 'insurance_service_category', ); } return $category; } /** * شناسهٔ بیمهٔ معتبر برای این نوبت، یا null وقتی انتخاب پاک شده است. * قرارداد باید فعال باشد و نوعش با جایگاهی که در آن انتخاب شده بخواند — * بیمهٔ تکمیلی نمی‌تواند جای بیمهٔ پایه بنشیند و برعکس. */ private function resolveInsuranceId(mixed $raw, string $entityType, int $entityId, bool $supplementary): ?int { $field = $supplementary ? 'insurance_supplementary_id' : 'insurance_base_id'; if ($raw === null || $raw === '' || (int) $raw <= 0) { return null; } $insuranceId = (int) $raw; $contract = $this->tenantInsuranceRepo->findActiveContract($entityType, $entityId, $insuranceId); if ($contract === null) { throw new AppException( ErrorCodes::ERR_VALIDATION_001, 'این بیمه برای این پزشک/کلینیک قرارداد فعال ندارد', 422, $field, ); } // نوعِ قرارداد بر نوع کاتالوگ اولویت دارد — همان قاعدهٔ TenantInsuranceService. $kind = $contract->getKind() ?? $this->insuranceRepo->find($insuranceId)?->getType()->value; $isSupp = $kind === InsuranceType::Supplementary->value; if ($isSupp !== $supplementary) { throw new AppException( ErrorCodes::ERR_VALIDATION_001, $supplementary ? 'اینجا فقط بیمهٔ تکمیلی قابل انتخاب است' : 'اینجا فقط بیمهٔ پایه قابل انتخاب است', 422, $field, ); } return $insuranceId; } }