agdar/finance/services.py
2025-11-02 14:35:35 +03:00

304 lines
9.7 KiB
Python

"""
Finance business logic services.
This module contains service classes that encapsulate business logic
for financial operations, clearance checks, and payment processing.
"""
import logging
from datetime import date, timedelta
from decimal import Decimal
from typing import Dict, List, Optional, Tuple
from django.db import transaction
from django.utils import timezone
from finance.models import Invoice, Payment, Payer
logger = logging.getLogger(__name__)
class FinancialClearanceService:
"""Service class for financial clearance operations."""
@staticmethod
def check_clearance(patient, service_type: str = None) -> Tuple[bool, str]:
"""
Check if patient has financial clearance for service.
Args:
patient: Patient instance
service_type: Optional service type to check specific requirements
Returns:
Tuple[bool, str]: (is_cleared, message)
"""
try:
# Check for outstanding invoices
outstanding_invoices = Invoice.objects.filter(
patient=patient,
status__in=['ISSUED', 'PARTIALLY_PAID', 'OVERDUE']
)
if outstanding_invoices.exists():
total_outstanding = sum(
invoice.amount_due for invoice in outstanding_invoices
)
# Allow small outstanding amounts (less than 10 SAR)
if total_outstanding > Decimal('10.00'):
invoice_numbers = ', '.join([
inv.invoice_number for inv in outstanding_invoices[:3]
])
return False, (
f"Outstanding invoices must be paid. "
f"Total due: {total_outstanding} SAR. "
f"Invoices: {invoice_numbers}"
)
# Check for overdue invoices (stricter check)
overdue_invoices = Invoice.objects.filter(
patient=patient,
status='OVERDUE'
)
if overdue_invoices.exists():
return False, "Overdue invoices must be paid before check-in"
# Check if service requires pre-payment
if service_type:
requires_prepayment = FinancialClearanceService._check_prepayment_requirement(
patient, service_type
)
if requires_prepayment:
return False, f"Service '{service_type}' requires pre-payment"
# Check insurance coverage if applicable
active_payer = Payer.objects.filter(
patient=patient,
is_active=True,
payer_type='INSURANCE'
).first()
if active_payer:
# Verify insurance is not expired
# This would integrate with insurance verification system
logger.info(f"Patient {patient.mrn} has active insurance: {active_payer.name}")
logger.info(f"Financial clearance approved for patient {patient.mrn}")
return True, "Financial clearance approved"
except Exception as e:
logger.error(f"Error checking financial clearance for patient {patient.mrn}: {e}")
return False, f"Error checking financial clearance: {str(e)}"
@staticmethod
def _check_prepayment_requirement(patient, service_type: str) -> bool:
"""
Check if service requires pre-payment.
Args:
patient: Patient instance
service_type: Service type to check
Returns:
bool: True if pre-payment required
"""
# Define services that require pre-payment
prepayment_services = [
'SURGERY',
'PROCEDURE',
'IMAGING_ADVANCED',
]
# Check if service type requires pre-payment
return service_type.upper() in prepayment_services
@staticmethod
def get_outstanding_balance(patient) -> Decimal:
"""
Get total outstanding balance for patient.
Args:
patient: Patient instance
Returns:
Decimal: Total outstanding amount
"""
outstanding_invoices = Invoice.objects.filter(
patient=patient,
status__in=['ISSUED', 'PARTIALLY_PAID', 'OVERDUE']
)
total = sum(
invoice.amount_due for invoice in outstanding_invoices
)
return Decimal(str(total))
@staticmethod
def get_outstanding_invoices(patient) -> List[Invoice]:
"""
Get list of outstanding invoices for patient.
Args:
patient: Patient instance
Returns:
List[Invoice]: Outstanding invoices
"""
return list(Invoice.objects.filter(
patient=patient,
status__in=['ISSUED', 'PARTIALLY_PAID', 'OVERDUE']
).order_by('-issue_date'))
@staticmethod
@transaction.atomic
def process_payment(
invoice: Invoice,
amount: Decimal,
payment_method: str,
processed_by,
**kwargs
) -> Payment:
"""
Process a payment for an invoice.
Args:
invoice: Invoice instance
amount: Payment amount
payment_method: Payment method (CASH, CARD, etc.)
processed_by: User processing the payment
**kwargs: Additional payment fields
Returns:
Payment: Created payment instance
"""
# Create payment record
payment = Payment.objects.create(
tenant=invoice.tenant,
invoice=invoice,
payment_date=timezone.now(),
amount=amount,
method=payment_method,
status='COMPLETED',
processed_by=processed_by,
**kwargs
)
logger.info(
f"Payment processed: {payment.id} for invoice {invoice.invoice_number} "
f"Amount: {amount}"
)
return payment
@staticmethod
def check_insurance_eligibility(patient, service_type: str = None) -> Tuple[bool, str, Optional[Payer]]:
"""
Check if patient has active insurance coverage.
Args:
patient: Patient instance
service_type: Optional service type to check coverage
Returns:
Tuple[bool, str, Optional[Payer]]: (has_coverage, message, payer)
"""
active_payer = Payer.objects.filter(
patient=patient,
is_active=True,
payer_type='INSURANCE'
).first()
if not active_payer:
return False, "No active insurance coverage", None
# Check coverage percentage
if active_payer.coverage_percentage <= 0:
return False, f"Insurance {active_payer.name} has 0% coverage", active_payer
logger.info(
f"Patient {patient.mrn} has {active_payer.coverage_percentage}% "
f"coverage with {active_payer.name}"
)
return True, f"Covered {active_payer.coverage_percentage}% by {active_payer.name}", active_payer
class InvoiceService:
"""Service class for invoice operations."""
@staticmethod
@transaction.atomic
def create_invoice_from_appointment(appointment) -> Invoice:
"""
Create an invoice from a completed appointment.
Args:
appointment: Appointment instance
Returns:
Invoice: Created invoice instance
"""
from finance.models import InvoiceLineItem, Service
# Check if invoice already exists
existing_invoice = Invoice.objects.filter(
appointment=appointment
).first()
if existing_invoice:
logger.info(f"Invoice already exists for appointment {appointment.id}")
return existing_invoice
# Get service pricing
service = Service.objects.filter(
clinic=appointment.clinic,
code=appointment.service_type
).first()
if not service:
logger.warning(
f"No service found for {appointment.service_type} at {appointment.clinic.name_en}"
)
# Use default pricing
unit_price = Decimal('100.00')
else:
unit_price = service.base_price.amount
# Calculate amounts
subtotal = unit_price
tax_rate = Decimal('0.15') # 15% VAT
tax = subtotal * tax_rate
total = subtotal + tax
# Create invoice
invoice = Invoice.objects.create(
tenant=appointment.tenant,
patient=appointment.patient,
appointment=appointment,
issue_date=date.today(),
due_date=date.today() + timedelta(days=30),
subtotal=subtotal,
tax=tax,
total=total,
status='ISSUED'
)
# Create line item
InvoiceLineItem.objects.create(
invoice=invoice,
service=service,
description=f"{appointment.service_type} - {appointment.clinic.name_en}",
quantity=1,
unit_price=unit_price,
total=unit_price
)
logger.info(
f"Invoice created: {invoice.invoice_number} for appointment {appointment.appointment_number}"
)
return invoice