resolveEntity($user); return [$type, $id, null]; } $doctor = $this->doctorRepo->findByUuid($doctorUuid); if ($doctor === null) { return ['unknown', null, $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پزشک یافت نشد', 404)]; } if ($user->hasRole('ROLE_ADMIN') || $doctor->getUser()->getId() === $user->getId()) { return [EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), null]; } foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) { if ($this->permChecker->can($user, $clinic, 'services', $action)) { return [EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), null]; } } return ['unknown', null, $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'دسترسی ممنوع', 403)]; } /** * قرارداد بیمه به همان محیطی تعلق دارد که پرونده‌ها در آن ثبت می‌شوند، پس همان * رزولوِر مبنا است: مالک کلینیکی که خودش پزشک هم هست باید قراردادهای کلینیکش را * ببیند، نه مطب شخصی‌اش. */ private function resolveEntity(User $user): array { return $this->scopeResolver->resolve($user)->toLegacyTuple(); } // ── Public list ─────────────────────────────────────────────────────────── #[Route('/api/v1/insurances', methods: ['GET'])] public function list(Request $request): JsonResponse { $typeParam = $request->query->get('type'); $type = null; if ($typeParam !== null) { $type = InsuranceType::tryFrom($typeParam); } return $this->success(['data' => $this->withCoverageDefaults($this->insuranceRepo->findActive($type))]); } // ── Admin CRUD — Insurance ──────────────────────────────────────────────── #[Route('/api/v1/admin/insurance', methods: ['POST'])] #[IsGranted('ROLE_ADMIN')] public function create(Request $request): JsonResponse { $data = json_decode($request->getContent(), true) ?? []; $name = trim($data['name'] ?? ''); $typeVal = $data['type'] ?? null; if ($name === '') { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'name الزامی است', 422, 'name'); } $type = InsuranceType::tryFrom((string) $typeVal); if ($type === null) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'type باید basic یا supplementary باشد', 422, 'type'); } $insurance = new Insurance($name, $type); if (isset($data['logo_url'])) $insurance->setLogoUrl($data['logo_url']); if (isset($data['status'])) $insurance->setStatus((int) $data['status']); $this->insuranceRepo->save($insurance); return $this->success(['data' => $insurance->toArray()], 201); } #[Route('/api/v1/admin/insurance/{id}', methods: ['PATCH'])] #[IsGranted('ROLE_ADMIN')] public function update(int $id, Request $request): JsonResponse { $insurance = $this->insuranceRepo->find($id); if ($insurance === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه یافت نشد', 404); } $data = json_decode($request->getContent(), true) ?? []; if (isset($data['name'])) $insurance->setName($data['name']); if (isset($data['type'])) { $type = InsuranceType::tryFrom($data['type']); if ($type !== null) $insurance->setType($type); } if (array_key_exists('logo_url', $data)) $insurance->setLogoUrl($data['logo_url']); if (isset($data['status'])) $insurance->setStatus((int) $data['status']); $this->insuranceRepo->save($insurance); return $this->success(['data' => $insurance->toArray()]); } #[Route('/api/v1/admin/insurance/{id}', methods: ['DELETE'])] #[IsGranted('ROLE_ADMIN')] public function delete(int $id): JsonResponse { $insurance = $this->insuranceRepo->find($id); if ($insurance === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه یافت نشد', 404); } $this->insuranceRepo->remove($insurance); return $this->success(['message' => 'بیمه با موفقیت حذف شد']); } #[Route('/api/v1/admin/insurances', methods: ['GET'])] #[IsGranted('ROLE_ADMIN')] public function adminList(Request $request): JsonResponse { $page = max(1, (int) $request->query->get('page', 1)); $limit = min(100, max(1, (int) $request->query->get('limit', 15))); $search = trim((string) $request->query->get('search', '')); $typeParam = $request->query->get('type'); $qb = $this->insuranceRepo->createQueryBuilder('i'); if ($request->query->get('sort') === 'id') { $qb->orderBy('i.id', strtoupper((string) $request->query->get('order')) === 'DESC' ? 'DESC' : 'ASC'); } else { $qb->orderBy('i.name', 'ASC'); } if ($search !== '') { $qb->andWhere('i.name LIKE :s')->setParameter('s', '%' . $search . '%'); } if ($typeParam !== null && $typeParam !== '') { $qb->andWhere('i.type = :t')->setParameter('t', $typeParam); } $total = (clone $qb)->select('COUNT(i.id)')->getQuery()->getSingleScalarResult(); $rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)->getQuery()->getResult(); return $this->paginated($this->withCoverageDefaults($rows), (int) $total, $page, $limit); } /** * Insurance rows carrying their central coverage percentages, resolved in one * query for the whole page. * * @param Insurance[] $insurances * @return list> */ private function withCoverageDefaults(array $insurances): array { $defaults = $this->coverageDefaults->percentMapForMany( array_map(static fn(Insurance $i) => (int) $i->getId(), $insurances) ); return array_map( static fn(Insurance $i) => $i->toArray() + ['coverage_defaults' => $defaults[$i->getId()] ?? []], $insurances, ); } // ── Admin — central coverage percentages per service category ───────────── #[Route('/api/v1/admin/insurance/{id}/coverage-defaults', methods: ['GET'])] #[IsGranted('ROLE_ADMIN')] public function getCoverageDefaults(int $id): JsonResponse { if ($this->insuranceRepo->find($id) === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه یافت نشد', 404); } return $this->success([ 'insurance_id' => $id, 'categories' => $this->coverageDefaults->settingsRows($id), ]); } #[Route('/api/v1/admin/insurance/{id}/coverage-defaults', methods: ['PUT'])] #[IsGranted('ROLE_ADMIN')] public function saveCoverageDefaults(int $id, Request $request): JsonResponse { if ($this->insuranceRepo->find($id) === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه یافت نشد', 404); } $data = json_decode($request->getContent(), true) ?? []; $this->coverageDefaults->save($id, $data['categories'] ?? []); return $this->success([ 'insurance_id' => $id, 'categories' => $this->coverageDefaults->settingsRows($id), ]); } // ── Upload logo ─────────────────────────────────────────────────────────── #[Route('/api/v1/admin/insurance/{id}/upload-logo', methods: ['POST'])] #[IsGranted('ROLE_ADMIN')] public function uploadLogo(int $id, Request $request): JsonResponse { $insurance = $this->insuranceRepo->find($id); if ($insurance === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه یافت نشد', 404); } $content = $request->getContent(); $disposition = $request->headers->get('Content-Disposition', ''); preg_match('/filename=["\']?([^"\';\s]+)["\']?/i', $disposition, $m); $filename = $m[1] ?? 'logo.jpg'; $tmpPath = sys_get_temp_dir() . '/' . uniqid('upload_', true); file_put_contents($tmpPath, $content); try { $safeFilename = $this->fileValidator->sanitizeFilename($filename); $mime = $this->fileValidator->detectMimeType($tmpPath); $year = date('Y'); $month = date('m'); $dir = $this->projectDir . '/public/uploads/insurances/logo/' . $year . '-' . $month; if (!is_dir($dir)) mkdir($dir, 0755, true); $storedName = uniqid('', true) . '_' . $safeFilename; rename($tmpPath, $dir . '/' . $storedName); $url = '/uploads/insurances/logo/' . $year . '-' . $month . '/' . $storedName; $insurance->setLogoUrl($url); $this->insuranceRepo->save($insurance); return $this->success([ 'url' => $url, 'uuid' => Uuid::v4()->toRfc4122(), 'filename' => $safeFilename, 'filemime' => $mime, 'filesize' => strlen($content), ]); } catch (\Throwable $e) { if (file_exists($tmpPath)) unlink($tmpPath); return $this->error(ErrorCodes::ERR_VALIDATION_001, $e->getMessage(), 422); } } // ── Entity insurance pricing (visit price by insurance) ─────────────────── #[Route('/api/v1/insurance-pricing', methods: ['GET'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function getInsurancePricing(Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'view'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'view'); [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view'); if ($err !== null) { return $err; } if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } return $this->success($this->pricingPayload($entityType, $entityId)); } private function pricingPayload(string $entityType, int $entityId): array { $rows = $this->pricingRepo->findByEntity($entityType, $entityId); $freeVisitPriceRials = 0; $requireVisitPrice = false; $perInsurance = []; foreach ($rows as $row) { if ($row->isFreeVisit()) { $freeVisitPriceRials = $row->getPatientShareRials(); $requireVisitPrice = $row->isRequireVisitPrice(); } else { $perInsurance[$row->getInsuranceId()] = $row->getPatientShareRials(); } } $catalog = $this->insuranceRepo->findActive(null); $defaults = $this->coverageDefaults->percentMapForMany( array_map(static fn(Insurance $i) => (int) $i->getId(), $catalog) ); $insurances = array_map(function (Insurance $i) use ($perInsurance, $defaults) { return [ 'insurance_id' => $i->getId(), 'insurance_name' => $i->getName(), 'type' => $i->getType()->value, 'patient_share_rials' => $perInsurance[$i->getId()] ?? null, 'coverage_defaults' => $defaults[$i->getId()] ?? [], ]; }, $catalog); return [ 'entity_type' => $entityType, 'entity_id' => $entityId, 'free_visit_price_rials' => $freeVisitPriceRials, 'require_visit_price' => $requireVisitPrice, 'insurances' => $insurances, // نوع خدماتِ بیمه‌ایِ این tenant — سراسری برای همهٔ بیمه‌ها. 'service_categories' => $this->serviceCategories->settingsRows($entityType, $entityId), // null یعنی چند نوع فعال است و کاربر باید سرِ پذیرش انتخاب کند. 'default_service_category' => $this->serviceCategories->defaultCategory($entityType, $entityId)?->value, ]; } #[Route('/api/v1/insurance-pricing', methods: ['PUT'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function saveInsurancePricing(Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'update'); $data = json_decode($request->getContent(), true) ?? []; [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update'); if ($err !== null) { return $err; } if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } $freeVisitRow = $this->pricingRepo->findOneForInsurance($entityType, $entityId, null); $requireVisitPrice = array_key_exists('require_visit_price', $data) ? (bool) $data['require_visit_price'] : ($freeVisitRow?->isRequireVisitPrice() ?? false); $freeVisitPrice = array_key_exists('free_visit_price_rials', $data) ? (int) $data['free_visit_price_rials'] : ($freeVisitRow?->getPatientShareRials() ?? 0); if ($requireVisitPrice && $freeVisitPrice <= 0) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'با فعال بودن «الزامی کردن هزینه ویزیت»، قیمت ویزیت آزاد الزامی است', 422, 'free_visit_price_rials'); } $touchesFreeVisit = array_key_exists('free_visit_price_rials', $data) || array_key_exists('require_visit_price', $data); if ($touchesFreeVisit && ($freeVisitRow !== null || $freeVisitPrice > 0)) { $this->upsertPricing($entityType, $entityId, null, $freeVisitPrice) ->setRequireVisitPrice($requireVisitPrice); } foreach (($data['insurances'] ?? []) as $row) { $insuranceId = isset($row['insurance_id']) ? (int) $row['insurance_id'] : null; if ($insuranceId === null) { continue; } if (!array_key_exists('patient_share_rials', $row) || $row['patient_share_rials'] === null) { $existing = $this->pricingRepo->findOneForInsurance($entityType, $entityId, $insuranceId); if ($existing !== null) { $this->pricingRepo->remove($existing, false); } continue; } $this->upsertPricing($entityType, $entityId, $insuranceId, (int) $row['patient_share_rials']); } $this->pricingRepo->getEntityManager()->flush(); if (array_key_exists('service_categories', $data)) { $this->serviceCategories->save($entityType, $entityId, (array) ($data['service_categories'] ?? [])); } return $this->success($this->pricingPayload($entityType, $entityId)); } private function upsertPricing(string $entityType, int $entityId, ?int $insuranceId, int $shareRials): EntityInsurancePricing { $row = $this->pricingRepo->findOneForInsurance($entityType, $entityId, $insuranceId); if ($row === null) { $row = new EntityInsurancePricing($entityType, $entityId, $insuranceId, $shareRials); } else { $row->setPatientShareRials($shareRials); } $this->pricingRepo->save($row, false); return $row; } // ── TenantInsurance — قراردادهای بیمه‌ی tenant ───────────────────────────── #[Route('/api/v1/billing/tenant-insurances', methods: ['GET'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function listTenantInsurances(Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'view'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'view'); [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view'); if ($err !== null) { return $err; } if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } $contracts = $this->tenantInsuranceRepo->findLatestByTenant($entityType, $entityId); $byId = []; foreach ($this->insuranceRepo->findActive(null) as $ins) { $byId[$ins->getId()] = ['name' => $ins->getName(), 'type' => $ins->getType()->value]; } $coverageView = $this->tenantInsuranceService->categoryCoverageViewForMany($contracts); $data = array_map(function (TenantInsurance $c) use ($byId, $coverageView) { $row = $c->toArray(); $row['insurance_name'] = $byId[$c->getInsuranceId()]['name'] ?? null; // Contract-level kind wins over the catalog type when the tenant categorised it. $row['insurance_kind'] = $c->getKind() ?? ($byId[$c->getInsuranceId()]['type'] ?? null); return $row + $coverageView[$c->getId()]; }, $contracts); return $this->success(['data' => $data]); } #[Route('/api/v1/billing/tenant-insurances', methods: ['POST'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function activateTenantInsurance(Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'create'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'create'); $data = json_decode($request->getContent(), true) ?? []; [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update'); if ($err !== null) { return $err; } if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } $insuranceId = isset($data['insurance_id']) ? (int) $data['insurance_id'] : 0; if ($insuranceId <= 0) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'insurance_id الزامی است', 422); } $contract = $this->tenantInsuranceService->activate( $entityType, $entityId, $insuranceId, (float) ($data['coverage_percent'] ?? 0), (int) ($data['franchise_rials'] ?? 0), isset($data['annual_ceiling_rials']) && $data['annual_ceiling_rials'] !== null ? (int) $data['annual_ceiling_rials'] : null, isset($data['effective_from']) && $data['effective_from'] !== null ? (int) $data['effective_from'] : null, isset($data['effective_to']) && $data['effective_to'] !== null ? (int) $data['effective_to'] : null, isset($data['kind']) && $data['kind'] !== '' ? (string) $data['kind'] : null, ); if (($err = $this->applyCategoryCoverages($contract, $data, $user)) !== null) { return $err; } return $this->success(['data' => $this->tenantInsuranceRow($contract)], 201); } /** * Persists the optional per-category overrides of a contract. Sending nothing keeps * the contract on the central admin defaults; overriding needs the update permission. */ private function applyCategoryCoverages(TenantInsurance $contract, array $data, User $user): ?JsonResponse { if (!array_key_exists('category_coverages', $data)) { return null; } if (!$this->secretaryAccess->canOrNonSecretary($user, 'insurances', 'update') || !$this->clinicDoctorAccess->canOrNonMember($user, 'insurances', 'update')) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'اجازه‌ی تغییر درصد پوشش را ندارید', 403); } $this->tenantInsuranceService->setCategoryCoverages($contract, $data['category_coverages'] ?? []); return null; } /** @return array contract row carrying its effective category percentages */ private function tenantInsuranceRow(TenantInsurance $contract): array { return $contract->toArray() + $this->tenantInsuranceService->categoryCoverageView($contract); } #[Route('/api/v1/billing/tenant-insurances/{uuid}', methods: ['PATCH'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function updateTenantInsurance(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'update'); $data = json_decode($request->getContent(), true) ?? []; [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update'); if ($err !== null) { return $err; } $contract = $this->tenantInsuranceRepo->findByUuid($uuid); if ($contract === null || $contract->getEntityType() !== $entityType || $contract->getEntityId() !== $entityId) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'قرارداد یافت نشد', 404); } if (array_key_exists('coverage_percent', $data)) { $contract->setCoveragePercent((float) $data['coverage_percent']); } if (array_key_exists('franchise_rials', $data)) { $contract->setFranchiseRials((int) $data['franchise_rials']); } if (array_key_exists('annual_ceiling_rials', $data)) { $contract->setAnnualCeilingRials($data['annual_ceiling_rials'] !== null ? (int) $data['annual_ceiling_rials'] : null); } if (array_key_exists('kind', $data)) { $contract->setKind($data['kind'] !== '' && $data['kind'] !== null ? (string) $data['kind'] : null); } if (array_key_exists('effective_from', $data) && $data['effective_from'] !== null) { $contract->setEffectiveFrom((int) $data['effective_from']); } if (array_key_exists('effective_to', $data)) { $contract->setEffectiveTo($data['effective_to'] !== null ? (int) $data['effective_to'] : null); } // Status toggle (فعال/غیرفعال) is set here directly so it does not clobber the // user-chosen effective_to the way the DELETE/deactivate path does. if (array_key_exists('is_active', $data)) { $contract->setActive((bool) $data['is_active']); } $this->tenantInsuranceRepo->save($contract); if (($err = $this->applyCategoryCoverages($contract, $data, $user)) !== null) { return $err; } return $this->success(['data' => $this->tenantInsuranceRow($contract)]); } #[Route('/api/v1/billing/tenant-insurances/{uuid}', methods: ['DELETE'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function deactivateTenantInsurance(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'delete'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'delete'); [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'update'); if ($err !== null) { return $err; } $contract = $this->tenantInsuranceRepo->findByUuid($uuid); if ($contract === null || $contract->getEntityType() !== $entityType || $contract->getEntityId() !== $entityId) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'قرارداد یافت نشد', 404); } $this->tenantInsuranceService->deactivate($contract); return $this->success(['message' => 'قرارداد بیمه غیرفعال شد']); } #[Route('/api/v1/billing/tenant-insurances/{uuid}/service-coverage', methods: ['GET'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function listServiceCoverage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'view'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'view'); [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view'); if ($err !== null) { return $err; } $contract = $this->tenantInsuranceRepo->findByUuid($uuid); if ($contract === null || $contract->getEntityType() !== $entityType || $contract->getEntityId() !== $entityId) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'قرارداد یافت نشد', 404); } $rows = $this->serviceCoverageRepo->findByContract($contract->getId()); // یک کوئری اسکالر برای همهٔ uuidها. هیدریت‌کردن entity کافی نیست: رابطهٔ // EAGER staffMembers روی ServiceItem به ازای هر ردیف یک کوئری اضافه می‌زند. $itemIds = array_values(array_unique(array_map(fn($r) => $r->getServiceItemId(), $rows))); $uuidById = $this->serviceItemRepo->findUuidsByIds($itemIds); $data = array_map(function ($r) use ($uuidById) { $row = $r->toArray(); $row['service_item_uuid'] = $uuidById[$r->getServiceItemId()] ?? null; return $row; }, $rows); return $this->success(['data' => $data]); } #[Route('/api/v1/billing/tenant-insurances/{uuid}/service-coverage', methods: ['PUT'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function setServiceCoverage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'update'); $data = json_decode($request->getContent(), true) ?? []; [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $data['doctor_uuid'] ?? null, 'update'); if ($err !== null) { return $err; } $contract = $this->tenantInsuranceRepo->findByUuid($uuid); if ($contract === null || $contract->getEntityType() !== $entityType || $contract->getEntityId() !== $entityId) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'قرارداد یافت نشد', 404); } $serviceItem = isset($data['service_item_uuid']) ? $this->serviceItemRepo->findByUuid((string) $data['service_item_uuid']) : (isset($data['service_item_id']) ? $this->serviceItemRepo->find((int) $data['service_item_id']) : null); if ($serviceItem === null) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'سرویس یافت نشد', 422); } $section = $serviceItem->getSection(); if ($section->getEntityType() !== $entityType || $section->getEntityId() !== $entityId) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'این سرویس متعلق به شما نیست', 403); } $serviceItemId = $serviceItem->getId(); $this->tenantInsuranceService->setServiceCoverage( $contract, $serviceItemId, (bool) ($data['covered'] ?? true), isset($data['coverage_percent']) && $data['coverage_percent'] !== null ? (float) $data['coverage_percent'] : null, isset($data['franchise_rials']) && $data['franchise_rials'] !== null ? (int) $data['franchise_rials'] : null, isset($data['ceiling_rials']) && $data['ceiling_rials'] !== null ? (int) $data['ceiling_rials'] : null, ); return $this->success(['message' => 'پوشش خدمت ذخیره شد']); } // ── DoctorInsurance CRUD ────────────────────────────────────────────────── #[Route('/api/v1/insurance/', methods: ['POST'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function addDoctorInsurance(Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'create'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'create'); $data = json_decode($request->getContent(), true) ?? []; $doctorId = $data['doctor_id'] ?? null; $insuranceId = $data['insurance_id'] ?? null; if (!$doctorId || !$insuranceId) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'doctor_id و insurance_id الزامی است', 422); } $doctor = $this->doctorRepo->find((int) $doctorId); if ($doctor === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); } if ($doctor->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } $insurance = $this->insuranceRepo->find((int) $insuranceId); if ($insurance === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه یافت نشد', 404); } $existing = $this->doctorInsuranceRepo->findOneBy(['doctor' => $doctor, 'insurance' => $insurance]); if ($existing !== null) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'این بیمه قبلاً اضافه شده است', 409); } $doctorInsurance = new DoctorInsurance($doctor, $insurance); if (isset($data['price'])) { $doctorInsurance->setPrice((int) $data['price']); } $this->doctorInsuranceRepo->save($doctorInsurance); return $this->success(['data' => $doctorInsurance->toArray()], 201); } #[Route('/api/v1/insurance/{id}', methods: ['GET'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function showDoctorInsurance(int $id, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'view'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'view'); $doctorInsurance = $this->doctorInsuranceRepo->find($id); if ($doctorInsurance === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه پزشک یافت نشد', 404); } if ($doctorInsurance->getDoctor()->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } return $this->success(['data' => $doctorInsurance->toArray()]); } #[Route('/api/v1/insurance/{id}', methods: ['PATCH'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function updateDoctorInsurance(int $id, Request $request, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'update'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'update'); $doctorInsurance = $this->doctorInsuranceRepo->find($id); if ($doctorInsurance === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه پزشک یافت نشد', 404); } if ($doctorInsurance->getDoctor()->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } $data = json_decode($request->getContent(), true) ?? []; if (array_key_exists('price', $data)) { $doctorInsurance->setPrice($data['price'] !== null ? (int) $data['price'] : null); } $this->doctorInsuranceRepo->save($doctorInsurance); return $this->success(['data' => $doctorInsurance->toArray()]); } #[Route('/api/v1/insurance/{id}', methods: ['DELETE'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function deleteDoctorInsurance(int $id, #[CurrentUser] User $user): JsonResponse { $this->secretaryAccess->denyUnlessGranted($user, 'insurances', 'delete'); $this->clinicDoctorAccess->denyUnlessGranted($user, 'insurances', 'delete'); $doctorInsurance = $this->doctorInsuranceRepo->find($id); if ($doctorInsurance === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'بیمه پزشک یافت نشد', 404); } if ($doctorInsurance->getDoctor()->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } $this->doctorInsuranceRepo->remove($doctorInsurance); return $this->success(['message' => 'بیمه پزشک با موفقیت حذف شد']); } }