feat: add visit price requirement feature

- Introduced a new boolean flag `require_visit_price` in the `EntityInsurancePricing` to enforce visit price for appointments.
- Updated the appointment creation endpoints to validate `visit_price_rials` based on the new flag.
- Added `visit_price_rials` field to the `Appointment` entity to store the visit price.
- Enhanced the `PatientService` to validate visit price during session creation.
- Updated API documentation to reflect changes in appointment and insurance pricing.
- Implemented a new service `VisitPriceRequirementResolver` to determine if a visit price is required for a doctor based on their pricing settings.
- Added migrations to update the database schema for the new fields.
This commit is contained in:
hamed
2026-07-16 19:44:30 +03:30
parent 880c4c06cb
commit a0ddb4c0d1
18 changed files with 497 additions and 22 deletions
@@ -229,10 +229,12 @@ class InsuranceController extends BaseController
$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();
}
@@ -251,6 +253,7 @@ class InsuranceController extends BaseController
'entity_type' => $entityType,
'entity_id' => $entityId,
'free_visit_price_rials' => $freeVisitPriceRials,
'require_visit_price' => $requireVisitPrice,
'insurances' => $insurances,
]);
}
@@ -266,8 +269,24 @@ class InsuranceController extends BaseController
$data = json_decode($request->getContent(), true) ?? [];
if (array_key_exists('free_visit_price_rials', $data)) {
$this->upsertPricing($entityType, $entityId, null, (int) $data['free_visit_price_rials']);
$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) {
@@ -290,7 +309,7 @@ class InsuranceController extends BaseController
return $this->getInsurancePricing($user);
}
private function upsertPricing(string $entityType, int $entityId, ?int $insuranceId, int $shareRials): void
private function upsertPricing(string $entityType, int $entityId, ?int $insuranceId, int $shareRials): EntityInsurancePricing
{
$row = $this->pricingRepo->findOneForInsurance($entityType, $entityId, $insuranceId);
if ($row === null) {
@@ -299,6 +318,8 @@ class InsuranceController extends BaseController
$row->setPatientShareRials($shareRials);
}
$this->pricingRepo->save($row, false);
return $row;
}
// ── TenantInsurance — قراردادهای بیمه‌ی tenant ─────────────────────────────
@@ -31,6 +31,10 @@ class EntityInsurancePricing
#[ORM\Column(name: 'patient_share_rials', type: 'integer')]
private int $patientShareRials = 0;
/** Only meaningful on the free-visit row (insurance_id = NULL). */
#[ORM\Column(name: 'require_visit_price', type: 'boolean', options: ['default' => false])]
private bool $requireVisitPrice = false;
#[ORM\Column(name: 'updated_at', type: 'integer')]
private int $updatedAt;
@@ -50,8 +54,10 @@ class EntityInsurancePricing
public function getPatientShareRials(): int { return $this->patientShareRials; }
public function isFreeVisit(): bool { return $this->insuranceId === null; }
public function isRequireVisitPrice(): bool { return $this->requireVisitPrice; }
public function setPatientShareRials(int $v): self { $this->patientShareRials = $v; $this->updatedAt = time(); return $this; }
public function setRequireVisitPrice(bool $v): self { $this->requireVisitPrice = $v; $this->updatedAt = time(); return $this; }
public function toArray(): array
{
@@ -61,6 +67,7 @@ class EntityInsurancePricing
'entity_id' => $this->entityId,
'insurance_id' => $this->insuranceId,
'patient_share_rials' => $this->patientShareRials,
'require_visit_price' => $this->requireVisitPrice,
];
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Insurance\Service;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Entity\Doctor;
use App\Insurance\Entity\EntityInsurancePricing;
use App\Insurance\Repository\EntityInsurancePricingRepository;
/**
* فلگ «الزامی کردن هزینه ویزیت» را برای پزشکِ یک نوبت resolve می‌کند:
* ردیف قیمت‌گذاری خود پزشک اگر موجود باشد؛ وگرنه کلینیکِ واحد پزشک
* (همان ترتیب resolve کردن tenant در PatientService).
*/
class VisitPriceRequirementResolver
{
public function __construct(
private readonly EntityInsurancePricingRepository $pricingRepo,
private readonly ClinicRepository $clinicRepo,
) {}
public function isRequiredForDoctor(Doctor $doctor): bool
{
$row = $this->pricingRepo->findOneForInsurance(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), null);
if ($row !== null) {
return $row->isRequireVisitPrice();
}
$clinics = $this->clinicRepo->findByDoctor($doctor);
if (count($clinics) === 1) {
return $this->pricingRepo
->findOneForInsurance(EntityInsurancePricing::TYPE_CLINIC, $clinics[0]->getId(), null)
?->isRequireVisitPrice() ?? false;
}
return false;
}
}