Files
clinicpro/tests/Billing/ClinicAppointmentUsesDoctorContractTest.php
T
hamedandClaude Opus 5 6e3d574e38 fix(session): show and keep the doctor's insurance when editing an encounter
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>
2026-08-18 17:56:57 +03:30

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());
}
}