The encounter edit form asked for contracts in the panel's own environment, so inside a clinic it got an empty list: no insurance to choose, and the one already recorded on the encounter had no matching option to display. It now scopes the request by the encounter's doctor, the same way the confirm modal does, which is why the session payload's existing doctor_uuid is threaded through. Saving the form had the matching defect on the server. Recalculating the totals looked up the coverage percentage against the record's tenant rather than the contract's, so a single save on a correctly-billed encounter zeroed the insurance share and moved the whole amount back onto the patient. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
170 lines
7.6 KiB
PHP
170 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Billing;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Auth\Entity\User;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Insurance\Entity\Insurance;
|
|
use App\Insurance\Enum\InsuranceType;
|
|
use App\Patient\Service\PatientService;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* نوبتِ کلینیک، قراردادِ پزشک: سهم بیمه باید واقعاً کسر شود.
|
|
*
|
|
* صورتحساب و مراجعه به محیط کلینیک تعلق دارند، ولی قرارداد بیمه روی خودِ پزشک ثبت
|
|
* شده است. تا پیش از این، درصد پوشش از محیط کلینیک پرسیده میشد، صفر برمیگشت، و
|
|
* سه چیز پشت سر هم خراب میشد: مراجعه کل مبلغ را از بیمار میخواست، صفحهٔ پرداخت
|
|
* ماندهٔ نادرست نشان میداد، و چون سهم بیمه صفر بود هیچ مطالبهای هم ساخته نمیشد.
|
|
*/
|
|
class ClinicAppointmentUsesDoctorContractTest extends ApiTestCase
|
|
{
|
|
private const VISIT_PRICE = 3_300_000;
|
|
|
|
/** @return array{0: User, 1: Doctor, 2: Clinic, 3: Insurance} */
|
|
private function scenario(int $coveragePercent): array
|
|
{
|
|
$doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
|
$doctor = new Doctor($doctorUser, 'دکتر قرارداد شخصی');
|
|
$doctor->setMobileNumber($doctorUser->getMobileNumber());
|
|
$this->em->persist($doctor);
|
|
|
|
$owner = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
|
$clinic = new Clinic($owner);
|
|
$clinic->setName('کلینیک مطالبه');
|
|
$clinic->getDoctors()->add($doctor);
|
|
$this->em->persist($clinic);
|
|
|
|
$insurance = new Insurance('بیمه مطالبه ' . random_int(1000, 9999), InsuranceType::Basic);
|
|
$this->em->persist($insurance);
|
|
$this->em->flush();
|
|
|
|
// قرارداد روی خودِ پزشک — همان کاری که پنل کلینیک میکند.
|
|
$this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
|
|
'doctor_uuid' => $doctor->getUuid(),
|
|
'insurance_id' => $insurance->getId(),
|
|
'coverage_percent' => $coveragePercent,
|
|
]);
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
return [$owner, $doctor, $clinic, $insurance];
|
|
}
|
|
|
|
private function appointment(Doctor $doctor, Clinic $clinic, Insurance $insurance): Appointment
|
|
{
|
|
$appointment = $this->newAppointment(
|
|
$doctor,
|
|
$this->createUser(['ROLE_USER']),
|
|
time() + 3600,
|
|
time() + 5400,
|
|
$clinic,
|
|
);
|
|
$appointment->setVisitPriceRials(self::VISIT_PRICE);
|
|
$appointment->setInsuranceBaseId($insurance->getId());
|
|
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
|
$this->em->persist($appointment);
|
|
$this->em->flush();
|
|
|
|
return $appointment;
|
|
}
|
|
|
|
public function testTheInsuranceShareIsDeductedFromWhatThePatientOwes(): void
|
|
{
|
|
[, $doctor, $clinic, $insurance] = $this->scenario(30);
|
|
$appointment = $this->appointment($doctor, $clinic, $insurance);
|
|
|
|
$session = static::getContainer()->get(PatientService::class)
|
|
->autoCreateOnAppointmentConfirm($appointment);
|
|
|
|
self::assertNotNull($session);
|
|
self::assertSame(990_000, $session->getBaseInsuranceRials(), 'سهم بیمه ۳۰٪ ویزیت است');
|
|
self::assertSame(2_310_000, $session->getPatientShareRials());
|
|
self::assertSame(2_310_000, $session->getRemainingRials(), 'مانده نباید شامل سهم بیمه باشد');
|
|
}
|
|
|
|
public function testTheInvoiceCarriesTheInsuranceShareSoAClaimCanBeBuilt(): void
|
|
{
|
|
[, $doctor, $clinic, $insurance] = $this->scenario(30);
|
|
$appointment = $this->appointment($doctor, $clinic, $insurance);
|
|
|
|
$session = static::getContainer()->get(PatientService::class)
|
|
->autoCreateOnAppointmentConfirm($appointment);
|
|
|
|
$invoice = static::getContainer()->get(\App\Billing\Service\InvoiceService::class)
|
|
->createFromSession($session, 'clinic', (int) $clinic->getId());
|
|
|
|
$shares = array_map(
|
|
static fn ($item): int => $item->getBaseInsuranceRials(),
|
|
$invoice->getItems()->toArray(),
|
|
);
|
|
|
|
self::assertContains(990_000, $shares, 'خط ویزیت باید سهم بیمه داشته باشد');
|
|
|
|
// صورتحساب همانجا نهایی میشود و مطالبهٔ بیمه از روی همین سهم ساخته میشود؛
|
|
// با سهمِ صفر هیچ مطالبهای ساخته نمیشد و صفحهٔ مطالبات خالی میماند.
|
|
$claims = $this->em->getRepository(\App\Billing\Entity\Claim::class)
|
|
->findBy(['entityType' => 'clinic', 'entityId' => (int) $clinic->getId()]);
|
|
|
|
self::assertNotSame([], $claims, 'باید برای سهم بیمه مطالبه ساخته شود');
|
|
self::assertSame(990_000, $claims[0]->getTotalClaimedRials());
|
|
}
|
|
|
|
/**
|
|
* ویرایش دستیِ همان مراجعه هم نباید سهم بیمه را دور بیندازد: فرم ویرایش سرویسها
|
|
* کل مبلغ را دوباره حساب میکند و اگر درصد را از محیط اشتباه بگیرد، مراجعهای که
|
|
* درست ساخته شده بود با یک ذخیرهٔ ساده خراب میشود.
|
|
*/
|
|
public function testEditingTheSessionKeepsTheInsuranceShare(): void
|
|
{
|
|
[$owner, $doctor, $clinic, $insurance] = $this->scenario(30);
|
|
$appointment = $this->appointment($doctor, $clinic, $insurance);
|
|
|
|
$session = static::getContainer()->get(PatientService::class)
|
|
->autoCreateOnAppointmentConfirm($appointment);
|
|
|
|
$this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [
|
|
'visit_price_rials' => self::VISIT_PRICE,
|
|
]);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
// درخواستِ کرنل روی EntityManager دیگری نوشت، پس ردیف دوباره خوانده میشود.
|
|
$row = $this->em->getConnection()->fetchAssociative(
|
|
'SELECT base_insurance_rials, patient_share_rials FROM patient_sessions WHERE uuid = ?',
|
|
[$session->getUuid()],
|
|
);
|
|
|
|
self::assertSame(990_000, (int) $row['base_insurance_rials']);
|
|
self::assertSame(2_310_000, (int) $row['patient_share_rials']);
|
|
}
|
|
|
|
/** نوبتِ بدون قرارداد، مثل قبل کاملاً سهم بیمار میماند. */
|
|
public function testAnAppointmentWithoutAnyContractStillBillsThePatientInFull(): void
|
|
{
|
|
[, $doctor, $clinic, ] = $this->scenario(30);
|
|
|
|
$other = new Insurance('بیمه بیقرارداد ' . random_int(1000, 9999), InsuranceType::Basic);
|
|
$this->em->persist($other);
|
|
$this->em->flush();
|
|
|
|
$appointment = $this->newAppointment(
|
|
$doctor,
|
|
$this->createUser(['ROLE_USER']),
|
|
time() + 7200,
|
|
time() + 9000,
|
|
$clinic,
|
|
);
|
|
$appointment->setVisitPriceRials(self::VISIT_PRICE);
|
|
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
|
$this->em->persist($appointment);
|
|
$this->em->flush();
|
|
|
|
$session = static::getContainer()->get(PatientService::class)
|
|
->autoCreateOnAppointmentConfirm($appointment);
|
|
|
|
self::assertSame(0, $session->getBaseInsuranceRials());
|
|
self::assertSame(self::VISIT_PRICE, $session->getPatientShareRials());
|
|
}
|
|
}
|