304 lines
9.7 KiB
Python
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
|