From f7eb1ebd9170d639f188c9dac438d08a3c3f4e6c Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Wed, 24 Jun 2026 02:08:12 +0330 Subject: [PATCH] feat(claims): implement automatic claim creation for insurance sessions --- docs/api/billing.md | 4 ++- docs/api/patient.md | 2 ++ src/Billing/Repository/ClaimRepository.php | 15 +++++++++ src/Billing/Service/ClaimService.php | 4 +++ src/Patient/Controller/PatientController.php | 32 ++++++++++++++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/api/billing.md b/docs/api/billing.md index 72c84133..e711da3c 100644 --- a/docs/api/billing.md +++ b/docs/api/billing.md @@ -97,6 +97,8 @@ مطالبه (`Claim`) از یک صورتحساب **نهایی‌شده** ساخته می‌شود: یک Claim برای بیمه‌ی پایه و یک Claim برای بیمه‌ی مکمل (فقط اگر سهم بیمه > ۰). هر `ClaimItem` به یک `InvoiceItem` ارجاع می‌دهد. چرخه‌ی وضعیت با state machine. +**ساخت خودکار:** هنگام ثبت session بیمه‌دار (`POST /api/v1/patient/{uuid}/session`)، زنجیره‌ی صورتحساب→نهایی‌سازی→مطالبه به‌صورت خودکار اجرا می‌شود؛ نیازی به فراخوانی دستی `POST /billing/claims` نیست. برای هر صورتحساب تنها یک‌بار مطالبه ساخته می‌شود (تلاش مجدد `422`). + **وضعیت‌ها:** `pending → submitted → {approved → paid | rejected}`. از `paid`/`rejected` خروجی ندارد. ## POST /api/v1/billing/claims @@ -104,7 +106,7 @@ **Body:** `{ "invoice_uuid": "…" }` **Response 201:** `{ success, data: [ …claims ] }` (یک یا دو مطالبه: پایه/مکمل). -**Errors:** `422` صورتحساب نهایی نشده / سهم بیمه ندارد · `404` صورتحساب یافت نشد. +**Errors:** `422` صورتحساب نهایی نشده / سهم بیمه ندارد / مطالبه از قبل ثبت شده · `404` صورتحساب یافت نشد. ## GET /api/v1/billing/claims?status= لیست مطالبات tenant. فیلتر اختیاری `status` (`pending|submitted|approved|rejected|paid`). diff --git a/docs/api/patient.md b/docs/api/patient.md index b89e6260..e586fd61 100644 --- a/docs/api/patient.md +++ b/docs/api/patient.md @@ -248,6 +248,8 @@ Creates a new visit session for a patient record. - `services`: array of service items to attach; `price_rials` snapshot از ServiceItem؛ `quantity` (پیش‌فرض ۱) → `line_total_rials = price_rials × quantity`. هر `SessionService` در پاسخ `quantity` و `line_total_rials` دارد. - `final_price_rials` is computed: `(visit_price × (1 - base%) × (1 - supp%)) + services_total` که `services_total = Σ(price × quantity)` +**اتصال خودکار مطالبه‌ی بیمه:** اگر session دارای `insurance_base_id` یا `insurance_supplementary_id` باشد، پس از ثبت به‌صورت خودکار صورتحساب ساخته و نهایی می‌شود و مطالبه(های) بیمه در وضعیت `pending` ایجاد می‌گردد (پایه/مکمل، فقط برای سهم بیمه > ۰). این مطالبات در صفحه‌ی [مطالبات بیمه](billing.md) قابل پیگیری و ارسال‌اند. خطا در این مرحله ثبت session را خراب نمی‌کند (لاگ می‌شود). برای هر صورتحساب فقط یک‌بار مطالبه ساخته می‌شود. + **Response 201:** ```json diff --git a/src/Billing/Repository/ClaimRepository.php b/src/Billing/Repository/ClaimRepository.php index 7e37a962..f652ab3d 100644 --- a/src/Billing/Repository/ClaimRepository.php +++ b/src/Billing/Repository/ClaimRepository.php @@ -67,6 +67,21 @@ class ClaimRepository extends ServiceEntityRepository }, $rows); } + /** آیا برای این صورتحساب از قبل مطالبه‌ای ساخته شده؟ (از طریق آیتم‌های صورتحساب) */ + public function existsForInvoice(int $invoiceId): bool + { + $count = (int) $this->createQueryBuilder('c') + ->select('COUNT(c.id)') + ->join('c.items', 'ci') + ->join('App\Billing\Entity\InvoiceItem', 'ii', 'WITH', 'ii.id = ci.invoiceItemId') + ->where('IDENTITY(ii.invoice) = :invoiceId') + ->setParameter('invoiceId', $invoiceId) + ->getQuery() + ->getSingleScalarResult(); + + return $count > 0; + } + public function save(Claim $entity, bool $flush = true): void { $this->getEntityManager()->persist($entity); diff --git a/src/Billing/Service/ClaimService.php b/src/Billing/Service/ClaimService.php index fc14ff08..aaa1811e 100644 --- a/src/Billing/Service/ClaimService.php +++ b/src/Billing/Service/ClaimService.php @@ -28,6 +28,10 @@ class ClaimService throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'فقط از صورتحساب نهایی‌شده می‌توان مطالبه ساخت', 422); } + if ($invoice->getId() !== null && $this->claimRepo->existsForInvoice($invoice->getId())) { + throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'برای این صورتحساب قبلاً مطالبه ثبت شده است', 422); + } + $claims = []; $baseId = $invoice->getBaseInsuranceId(); diff --git a/src/Patient/Controller/PatientController.php b/src/Patient/Controller/PatientController.php index f9d3e390..f2b5bfa6 100644 --- a/src/Patient/Controller/PatientController.php +++ b/src/Patient/Controller/PatientController.php @@ -5,6 +5,9 @@ namespace App\Patient\Controller; use App\Auth\Entity\User; use App\Auth\Repository\UserActiveContextRepository; use App\Auth\Repository\UserRepository; +use App\Billing\Service\ClaimService; +use App\Billing\Service\InvoiceService; +use Psr\Log\LoggerInterface; use App\Clinic\Repository\ClinicRepository; use App\Doctor\Repository\DoctorRepository; use App\Patient\Entity\PatientRecord; @@ -39,6 +42,9 @@ class PatientController extends BaseController private readonly UserActiveContextRepository $contextRepo, private readonly \App\UserProfile\Repository\UserProfileRepository $profileRepo, private readonly \App\Insurance\Repository\InsuranceRepository $insuranceRepo, + private readonly InvoiceService $invoiceService, + private readonly ClaimService $claimService, + private readonly LoggerInterface $logger, ) {} private function buildPatientProfile(User $patient): array @@ -221,9 +227,35 @@ class PatientController extends BaseController $data = json_decode($request->getContent(), true) ?? []; $session = $this->patientService->createSession($record, $data, $entityType, $entityId); + $this->autoCreateClaim($session, $entityType, $entityId); + return $this->success($session->toArray(), 201); } + /** + * اتصال خودکار مطالبه‌ی بیمه: اگر session بیمه‌ی پایه یا مکمل داشته باشد، + * صورتحساب ساخته و نهایی می‌شود و مطالبه (در وضعیت pending) ایجاد می‌گردد. + * ارسال مطالبه دستی از صفحه‌ی مطالبات انجام می‌شود. + * خطا در این مرحله نباید ثبت session را خراب کند. + */ + private function autoCreateClaim(\App\Patient\Entity\PatientSession $session, string $entityType, int $entityId): void + { + if ($session->getInsuranceBaseId() === null && $session->getInsuranceSupplementaryId() === null) { + return; + } + + try { + $invoice = $this->invoiceService->createFromSession($session, $entityType, $entityId); + $this->invoiceService->finalize($invoice); + $this->claimService->createFromInvoice($invoice); + } catch (\App\Shared\Exception\AppException $e) { + // سهم بیمه‌ای صفر بود یا صورتحساب از قبل وجود داشت — قابل چشم‌پوشی. + $this->logger->info('auto claim skipped', ['session' => $session->getUuid(), 'reason' => $e->getMessage()]); + } catch (\Throwable $e) { + $this->logger->error('auto claim failed', ['session' => $session->getUuid(), 'error' => $e->getMessage()]); + } + } + #[Route('/api/v1/session/{uuid}', methods: ['PATCH'])] public function updateSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse {