@@ -51,8 +51,38 @@ class PatientService
private readonly WalletService $walletService ,
private readonly EntityInsurancePricingRepository $pricingRepo ,
private readonly \App\Discount\Service\DiscountEngine $discountEngine ,
private readonly \App\Patient\Repository\SessionAuditLogRepository $auditRepo ,
) {}
/** ثبت یک رکورد تاریخچهی تغییر مالی/خدماتی روی مراجعه. */
public function logSessionChange ( PatientSession $session , string $field , string $operation , ? string $old , ? string $new , ? User $actor , ? string $note = null ) : void
{
$log = new \App\Patient\Entity\SessionAuditLog ( $session , $field , $operation );
$log -> setActor ( $actor ? -> getId (), $this -> walletService -> resolveActorName ( $actor ));
$log -> setValues ( $old , $new );
$log -> setNote ( $note );
$this -> auditRepo -> save ( $log );
}
/** خلاصهی خوانا از سرویسهای یک مراجعه: «نام ×تعداد، ...». */
private function servicesSummary ( PatientSession $session ) : string
{
$parts = array_map (
fn ( SessionService $s ) => ( $s -> toArray ()[ 'service_name' ] ? ? '?' ) . ' × ' . $s -> getQuantity (),
$session -> getServices () -> toArray (),
);
return $parts === [] ? '—' : implode ( '، ' , $parts );
}
private function consumablesSummary ( PatientSession $session ) : string
{
$parts = array_map (
fn ( SessionConsumable $c ) => ( $c -> toArray ()[ 'item_name' ] ? ? '?' ) . ' × ' . $c -> getQuantity (),
$session -> getConsumables () -> toArray (),
);
return $parts === [] ? '—' : implode ( '، ' , $parts );
}
/**
* محاسبهی سهم بیمار.
* ویزیت با درصد تخفیف انتخابشده در فرم؛ هر خدمت با قاعدهی پوشش همان بیمهگر برای همان خدمت
@@ -264,16 +294,187 @@ class PatientService
return $session ;
}
/**
* ویرایش سرویسها/کالاها/قیمت ویزیت/بیمهی یک مراجعه پس از ثبت، با بازمحاسبهی
* مجموعها و ثبت تاریخچه (audit) برای هر فیلد تغییرکرده.
*/
public function updateSessionServices ( PatientSession $session , array $data , string $entityType , int $entityId , User $actor ) : PatientSession
{
$before = [
'visit_price_rials' => ( string ) $session -> getVisitPriceRials (),
'services' => $this -> servicesSummary ( $session ),
'consumables' => $this -> consumablesSummary ( $session ),
'services_total_rials' => ( string ) $session -> getServicesTotalRials (),
'final_price_rials' => ( string ) $session -> getFinalPriceRials (),
];
// فیلدهای ساده
if ( array_key_exists ( 'visit_price_rials' , $data )) { $session -> setVisitPriceRials (( int ) $data [ 'visit_price_rials' ]); }
if ( array_key_exists ( 'insurance_base_id' , $data )) { $session -> setInsuranceBaseId ( $data [ 'insurance_base_id' ] !== null ? ( int ) $data [ 'insurance_base_id' ] : null ); }
if ( array_key_exists ( 'insurance_supplementary_id' , $data )) { $session -> setInsuranceSupplementaryId ( $data [ 'insurance_supplementary_id' ] !== null ? ( int ) $data [ 'insurance_supplementary_id' ] : null ); }
if ( array_key_exists ( 'base_insurance_discount_percent' , $data )) { $session -> setBaseInsuranceDiscountPercent (( float ) $data [ 'base_insurance_discount_percent' ]); }
if ( array_key_exists ( 'supplementary_discount_percent' , $data )) { $session -> setSupplementaryDiscountPercent (( float ) $data [ 'supplementary_discount_percent' ]); }
if ( array_key_exists ( 'notes' , $data )) { $session -> setNotes ( $data [ 'notes' ] !== null ? ( string ) $data [ 'notes' ] : null ); }
if ( ! empty ( $data [ 'session_at' ])) { $session -> setSessionAt (( int ) $data [ 'session_at' ]); }
// جایگزینی سرویسها (اگر ارسال شده)
if ( array_key_exists ( 'services' , $data )) {
foreach ( $session -> getServices () -> toArray () as $old ) {
$this -> sessionServiceRepo -> remove ( $old , false );
$session -> getServices () -> removeElement ( $old );
}
foreach (( $data [ 'services' ] ? ? []) as $svc ) {
$item = $this -> serviceItemRepo -> findByUuid ( $svc [ 'service_item_uuid' ] ? ? '' );
if ( $item === null ) { continue ; }
$staff = ! empty ( $svc [ 'staff_uuid' ]) ? $this -> staffRepo -> findByUuid ( $svc [ 'staff_uuid' ]) : null ;
$qty = max ( 1 , ( int ) ( $svc [ 'quantity' ] ? ? 1 ));
$ss = new SessionService ( $session , $item , $staff , $qty );
$this -> sessionServiceRepo -> save ( $ss );
$session -> addService ( $ss );
}
}
// جایگزینی کالاهای مصرفی (اگر ارسال شده)
if ( array_key_exists ( 'consumables' , $data )) {
foreach ( $session -> getConsumables () -> toArray () as $old ) {
$this -> sessionConsumableRepo -> remove ( $old , false );
$session -> getConsumables () -> removeElement ( $old );
}
foreach (( $data [ 'consumables' ] ? ? []) as $row ) {
$item = $this -> inventoryItemRepo -> findByUuid (( string ) ( $row [ 'inventory_item_uuid' ] ? ? '' ));
if ( $item === null || $item -> getEntityType () !== $entityType || $item -> getEntityId () !== $entityId ) { continue ; }
$qty = max ( 1 , ( int ) ( $row [ 'quantity' ] ? ? 1 ));
$sc = new SessionConsumable ( $session , $item , $qty );
$this -> sessionConsumableRepo -> save ( $sc );
$session -> addConsumable ( $sc );
}
}
// بازمحاسبهی مجموعها (مثل createSession)
$serviceItemsData = array_map (
fn ( SessionService $s ) => [ 'item_id' => $s -> getServiceItem () -> getId (), 'price_rials' => $s -> getLineTotalRials ()],
$session -> getServices () -> toArray (),
);
$priceCalc = $this -> calculateFinalPrice (
$session -> getVisitPriceRials (),
$session -> getBaseInsuranceDiscountPercent (),
$session -> getSupplementaryDiscountPercent (),
$serviceItemsData ,
$entityType ,
$entityId ,
$session -> getInsuranceBaseId (),
$session -> getInsuranceSupplementaryId (),
);
$session -> setServicesTotalRials ( $priceCalc [ 'services_total_rials' ]);
$session -> setFinalPriceRials ( $priceCalc [ 'final_price_rials' ] + $session -> getConsumablesTotalRials ());
$this -> sessionRepo -> save ( $session );
// ثبت audit برای فیلدهای تغییرکرده
$after = [
'visit_price_rials' => ( string ) $session -> getVisitPriceRials (),
'services' => $this -> servicesSummary ( $session ),
'consumables' => $this -> consumablesSummary ( $session ),
'services_total_rials' => ( string ) $session -> getServicesTotalRials (),
'final_price_rials' => ( string ) $session -> getFinalPriceRials (),
];
foreach ( $after as $field => $newVal ) {
if ( $before [ $field ] !== $newVal ) {
$this -> logSessionChange ( $session , $field , \App\Patient\Entity\SessionAuditLog :: OP_UPDATE , $before [ $field ], $newVal , $actor );
}
}
return $session ;
}
/**
* ویرایش یک پرداخت ثبتشده (روش/مبلغ/تاریخ) با ثبت audit و بازمحاسبهی
* فیلدهای کششدهی تسویه. پرداخت wallet مسدود است (جبران کیف پول خارج از scope).
*/
public function updatePayment ( SessionPayment $payment , array $data , User $actor ) : SessionPayment
{
$session = $payment -> getSession ();
if ( $payment -> getMethod () === 'wallet' ) {
throw new AppException ( ErrorCodes :: ERR_SESSION_PAYMENT_INVALID , 'ویرایش پرداخت کیف پول ممکن نیست' , 422 , 'method' );
}
$oldAmount = $payment -> getAmountRials ();
$newAmount = array_key_exists ( 'amount_rials' , $data ) ? max ( 0 , ( int ) $data [ 'amount_rials' ]) : $oldAmount ;
if ( $newAmount <= 0 ) {
throw new AppException ( ErrorCodes :: ERR_SESSION_PAYMENT_INVALID , null , 422 , 'amount_rials' );
}
// مجموع پرداختها (با مقدار جدید) نباید از مبلغِ پس از تخفیف بیشتر شود.
$othersTotal = $session -> getPaidTotalRials () - $oldAmount ;
$payable = $session -> getFinalPriceRials () - $session -> getDiscountRials ();
if ( $othersTotal + $newAmount > $payable ) {
throw new AppException ( ErrorCodes :: ERR_SESSION_PAYMENT_EXCEEDS , null , 422 , 'amount_rials' );
}
if ( array_key_exists ( 'method' , $data )) {
$method = ( string ) $data [ 'method' ];
if ( ! in_array ( $method , SessionPayment :: METHODS , true ) || $method === 'wallet' ) {
throw new AppException ( ErrorCodes :: ERR_SESSION_PAYMENT_INVALID , null , 422 , 'method' );
}
$payment -> setMethod ( $method );
}
$payment -> setAmountRials ( $newAmount );
if ( ! empty ( $data [ 'paid_at' ])) { $payment -> setPaidAt (( int ) $data [ 'paid_at' ]); }
$this -> sessionPaymentRepo -> save ( $payment );
$this -> recomputeSettlement ( $session );
$this -> logSessionChange ( $session , 'payment' , \App\Patient\Entity\SessionAuditLog :: OP_UPDATE , ( string ) $oldAmount , ( string ) $newAmount , $actor , 'ویرایش پرداخت' );
return $payment ;
}
/**
* حذف یک پرداخت ثبتشده با ثبت audit و بازمحاسبهی تسویه.
* پرداخت wallet مسدود است (جبران کیف پول خارج از scope).
*/
public function deletePayment ( SessionPayment $payment , User $actor ) : void
{
$session = $payment -> getSession ();
if ( $payment -> getMethod () === 'wallet' ) {
throw new AppException ( ErrorCodes :: ERR_SESSION_PAYMENT_INVALID , 'حذف پرداخت کیف پول ممکن نیست' , 422 , 'method' );
}
$amount = $payment -> getAmountRials ();
$session -> getPayments () -> removeElement ( $payment );
$this -> sessionPaymentRepo -> remove ( $payment );
$this -> recomputeSettlement ( $session );
$this -> logSessionChange ( $session , 'payment' , \App\Patient\Entity\SessionAuditLog :: OP_DELETE , ( string ) $amount , null , $actor , 'حذف پرداخت' );
}
/** بازمحاسبهی فیلدهای کششدهی تسویه پس از تغییر پرداختها. */
private function recomputeSettlement ( PatientSession $session ) : void
{
if ( $session -> getRemainingRials () === 0 && $session -> getPaidTotalRials () > 0 ) {
if ( $session -> getPaymentMethod () === 'pending' ) {
$session -> setPaymentMethod ( 'cash' );
}
if ( $session -> getPaidAt () === null ) {
$session -> setPaidAt ( time ());
}
} else {
$session -> setPaymentMethod ( 'pending' );
$session -> setPaidAt ( null );
}
$this -> sessionRepo -> save ( $session );
}
/**
* اعمال/حذف تخفیف تسویه روی مراجعه.
* type=null → حذف تخفیف. percent باید 0..100 و fixed حداکثر برابر مبلغ نهایی باشد.
* تخفیف نمیتواند از ماندهی قابلتخفیف (مبلغ نهایی منهای پرداختهای ثبتشده) بیشتر شود.
*/
public function applyDiscount ( PatientSession $session , ? string $type , int $value , ? int $ruleId = null , ? string $ruleLabel = null ) : PatientSession
public function applyDiscount ( PatientSession $session , ? string $type , int $value , ? int $ruleId = null , ? string $ruleLabel = null , ? User $actor = null ) : PatientSession
{
if ( $type === null ) {
$oldRials = $session -> getDiscountRials ();
$session -> setDiscount ( null , 0 , 0 );
$this -> sessionRepo -> save ( $session );
if ( $oldRials !== 0 ) {
$this -> logSessionChange ( $session , 'discount' , \App\Patient\Entity\SessionAuditLog :: OP_DELETE , ( string ) $oldRials , null , $actor );
}
return $session ;
}
@@ -299,7 +500,7 @@ class PatientService
throw new AppException ( ErrorCodes :: ERR_SESSION_DISCOUNT_INVALID , null , 422 , 'discount_value' );
}
$this -> persistDiscount ( $session , $type , $value , $rials , $ruleId , $ruleLabel );
$this -> persistDiscount ( $session , $type , $value , $rials , $ruleId , $ruleLabel , $actor );
return $session ;
}
@@ -308,19 +509,20 @@ class PatientService
* اعمال یک قانون تخفیف روی مراجعه؛ مبلغ ریالی از موتور (با در نظر گرفتن نوع/مبنا)
* محاسبه و منبع قانون برای audit ثبت میشود.
*/
public function applyDiscountRule ( PatientSession $session , \App\Discount\Entity\DiscountRule $rule ) : PatientSession
public function applyDiscountRule ( PatientSession $session , \App\Discount\Entity\DiscountRule $rule , ? User $actor = null ) : PatientSession
{
$rials = $this -> discountEngine -> computeForRule ( $session , $rule );
if ( $rials <= 0 ) {
throw new AppException ( ErrorCodes :: ERR_SESSION_DISCOUNT_INVALID , null , 422 , 'discount_rule_uuid' );
}
$this -> persistDiscount ( $session , $rule -> getDiscountType (), $rule -> getValue (), $rials , $rule -> getId (), $rule -> getName ());
$this -> persistDiscount ( $session , $rule -> getDiscountType (), $rule -> getValue (), $rials , $rule -> getId (), $rule -> getName (), $actor );
return $session ;
}
private function persistDiscount ( PatientSession $session , string $type , int $value , int $rials , ? int $ruleId , ? string $ruleLabel ) : void
private function persistDiscount ( PatientSession $session , string $type , int $value , int $rials , ? int $ruleId , ? string $ruleLabel , ? User $actor = null ) : void
{
$oldRials = $session -> getDiscountRials ();
$session -> setDiscount ( $type , $value , $rials , $ruleId , $ruleLabel );
if ( $session -> getRemainingRials () === 0 && $session -> getPaymentMethod () === 'pending' ) {
// تخفیف صددرصدی بدهی را صفر کرد — مراجعه تسویهشده تلقی میشود
@@ -328,6 +530,9 @@ class PatientService
$session -> setPaidAt ( time ());
}
$this -> sessionRepo -> save ( $session );
if ( $oldRials !== $rials ) {
$this -> logSessionChange ( $session , 'discount' , \App\Patient\Entity\SessionAuditLog :: OP_UPDATE , ( string ) $oldRials , ( string ) $rials , $actor , $ruleLabel );
}
}
/**