feat(claims): implement automatic claim creation for insurance sessions
This commit is contained in:
+3
-1
@@ -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`).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user