hospital-management/patients/management/commands/generate_saudi_claims.py
Marwan Alwali 610e165e17 update
2025-09-04 19:19:52 +03:00

399 lines
16 KiB
Python

"""
Django management command to generate Saudi-influenced insurance claims data.
Usage:
python manage.py generate_saudi_claims --count 100 --tenant-id 1
python manage.py generate_saudi_claims --count 500 --all-tenants
python manage.py generate_saudi_claims --count 50 --patient-id 123
"""
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone
from patients.models import PatientProfile, InsuranceInfo, InsuranceClaim, ClaimStatusHistory
from patients.data_generators.saudi_claims_generator import SaudiClaimsDataGenerator
from core.models import Tenant
import random
User = get_user_model()
class Command(BaseCommand):
help = 'Generate Saudi-influenced insurance claims data for testing and development'
def add_arguments(self, parser):
parser.add_argument(
'--count',
type=int,
default=50,
help='Number of claims to generate (default: 50)'
)
parser.add_argument(
'--tenant-id',
type=int,
help='Specific tenant ID to generate claims for'
)
parser.add_argument(
'--all-tenants',
action='store_true',
help='Generate claims for all tenants'
)
parser.add_argument(
'--patient-id',
type=int,
help='Generate claims for a specific patient'
)
parser.add_argument(
'--insurance-id',
type=int,
help='Generate claims for a specific insurance policy'
)
parser.add_argument(
'--claim-type',
choices=['MEDICAL', 'DENTAL', 'VISION', 'PHARMACY', 'EMERGENCY',
'INPATIENT', 'OUTPATIENT', 'PREVENTIVE', 'MATERNITY',
'MENTAL_HEALTH', 'REHABILITATION', 'DIAGNOSTIC', 'SURGICAL', 'CHRONIC_CARE'],
help='Generate claims of specific type only'
)
parser.add_argument(
'--status',
choices=['DRAFT', 'SUBMITTED', 'UNDER_REVIEW', 'APPROVED',
'PARTIALLY_APPROVED', 'DENIED', 'PAID', 'CANCELLED',
'APPEALED', 'RESUBMITTED'],
help='Generate claims with specific status only'
)
parser.add_argument(
'--days-back',
type=int,
default=180,
help='Generate claims from this many days back (default: 180)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be generated without creating records'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Show detailed output'
)
parser.add_argument(
'--clear-existing',
action='store_true',
help='Clear existing claims before generating new ones (USE WITH CAUTION)'
)
def handle(self, *args, **options):
"""Main command handler."""
self.verbosity = options.get('verbosity', 1)
self.verbose = options.get('verbose', False)
try:
# Validate options
self._validate_options(options)
# Get patients with insurance
patients_with_insurance = self._get_patients_with_insurance(options)
if not patients_with_insurance:
raise CommandError("No patients with insurance found. Please create patients and insurance records first.")
# Clear existing claims if requested
if options['clear_existing']:
self._clear_existing_claims(options)
# Generate claims
if options['dry_run']:
self._dry_run(patients_with_insurance, options)
else:
self._generate_claims(patients_with_insurance, options)
except Exception as e:
raise CommandError(f"Error generating claims: {str(e)}")
def _validate_options(self, options):
"""Validate command options."""
if options['tenant_id'] and options['all_tenants']:
raise CommandError("Cannot specify both --tenant-id and --all-tenants")
if options['count'] <= 0:
raise CommandError("Count must be a positive integer")
if options['days_back'] <= 0:
raise CommandError("Days back must be a positive integer")
def _get_patients_with_insurance(self, options):
"""Get patients with insurance based on options."""
patients_with_insurance = []
# Base queryset
patients_query = PatientProfile.objects.select_related('tenant')
insurance_query = InsuranceInfo.objects.select_related('patient', 'patient__tenant')
# Filter by tenant
if options['tenant_id']:
try:
tenant = Tenant.objects.get(id=options['tenant_id'])
patients_query = patients_query.filter(tenant=tenant)
insurance_query = insurance_query.filter(patient__tenant=tenant)
if self.verbose:
self.stdout.write(f"Filtering by tenant: {tenant.name}")
except Tenant.DoesNotExist:
raise CommandError(f"Tenant with ID {options['tenant_id']} not found")
elif not options['all_tenants']:
# Default to first tenant if no specific tenant specified
first_tenant = Tenant.objects.first()
if first_tenant:
patients_query = patients_query.filter(tenant=first_tenant)
insurance_query = insurance_query.filter(patient__tenant=first_tenant)
if self.verbose:
self.stdout.write(f"Using default tenant: {first_tenant.name}")
# Filter by specific patient
if options['patient_id']:
try:
patient = patients_query.get(id=options['patient_id'])
insurance_query = insurance_query.filter(patient=patient)
if self.verbose:
self.stdout.write(f"Filtering by patient: {patient.get_full_name()}")
except PatientProfile.DoesNotExist:
raise CommandError(f"Patient with ID {options['patient_id']} not found")
# Filter by specific insurance
if options['insurance_id']:
try:
insurance = insurance_query.get(id=options['insurance_id'])
patients_with_insurance = [(insurance.patient, insurance)]
if self.verbose:
self.stdout.write(f"Using specific insurance: {insurance.insurance_provider}")
except InsuranceInfo.DoesNotExist:
raise CommandError(f"Insurance with ID {options['insurance_id']} not found")
else:
# Get all patients with insurance
for insurance in insurance_query:
patients_with_insurance.append((insurance.patient, insurance))
return patients_with_insurance
def _clear_existing_claims(self, options):
"""Clear existing claims if requested."""
if not options['clear_existing']:
return
self.stdout.write(
self.style.WARNING("Clearing existing claims...")
)
# Build filter for claims to delete
claims_query = InsuranceClaim.objects.all()
if options['tenant_id']:
claims_query = claims_query.filter(patient__tenant_id=options['tenant_id'])
elif not options['all_tenants']:
first_tenant = Tenant.objects.first()
if first_tenant:
claims_query = claims_query.filter(patient__tenant=first_tenant)
if options['patient_id']:
claims_query = claims_query.filter(patient_id=options['patient_id'])
if options['insurance_id']:
claims_query = claims_query.filter(insurance_info_id=options['insurance_id'])
deleted_count = claims_query.count()
claims_query.delete()
self.stdout.write(
self.style.SUCCESS(f"Cleared {deleted_count} existing claims")
)
def _dry_run(self, patients_with_insurance, options):
"""Show what would be generated without creating records."""
self.stdout.write(
self.style.WARNING("DRY RUN - No records will be created")
)
generator = SaudiClaimsDataGenerator()
# Generate sample claims data
sample_claims = generator.generate_multiple_claims(
patients_with_insurance[:min(5, len(patients_with_insurance))],
min(5, options['count'])
)
self.stdout.write(f"\nWould generate {options['count']} claims")
self.stdout.write(f"Available patients with insurance: {len(patients_with_insurance)}")
if sample_claims:
self.stdout.write(f"\nSample claim data:")
for i, claim in enumerate(sample_claims[:3], 1):
self.stdout.write(f"\nClaim {i}:")
self.stdout.write(f" - Number: {claim['claim_number']}")
self.stdout.write(f" - Patient: {claim['patient'].get_full_name()}")
self.stdout.write(f" - Type: {claim['claim_type']}")
self.stdout.write(f" - Status: {claim['status']}")
self.stdout.write(f" - Amount: {claim['billed_amount']} SAR")
self.stdout.write(f" - Provider: {claim['service_provider']}")
self.stdout.write(f" - Facility: {claim['facility_name']}")
# Show statistics
stats = generator.get_saudi_insurance_statistics(sample_claims)
if stats:
self.stdout.write(f"\nSample statistics:")
self.stdout.write(f" - Total claims: {stats['total_claims']}")
self.stdout.write(f" - Approval rate: {stats['approval_rate']:.1f}%")
self.stdout.write(f" - Average amount: {stats['average_claim_amount_sar']:.2f} SAR")
def _generate_claims(self, patients_with_insurance, options):
"""Generate the actual claims records."""
generator = SaudiClaimsDataGenerator()
created_by = self._get_system_user()
self.stdout.write(f"Generating {options['count']} insurance claims...")
if self.verbose:
self.stdout.write(f"Available patients with insurance: {len(patients_with_insurance)}")
# Generate claims data
claims_data = generator.generate_multiple_claims(
patients_with_insurance,
options['count'],
created_by
)
# Filter by claim type if specified
if options['claim_type']:
claims_data = [c for c in claims_data if c['claim_type'] == options['claim_type']]
if self.verbose:
self.stdout.write(f"Filtered to {len(claims_data)} claims of type {options['claim_type']}")
# Filter by status if specified
if options['status']:
claims_data = [c for c in claims_data if c['status'] == options['status']]
if self.verbose:
self.stdout.write(f"Filtered to {len(claims_data)} claims with status {options['status']}")
if not claims_data:
self.stdout.write(
self.style.WARNING("No claims data generated after filtering")
)
return
# Create claims in database
created_claims = []
created_count = 0
with transaction.atomic():
for claim_data in claims_data:
try:
# Create the claim
claim = InsuranceClaim.objects.create(**claim_data)
created_claims.append(claim)
created_count += 1
# Create status history
self._create_status_history(claim, created_by)
if self.verbose and created_count % 10 == 0:
self.stdout.write(f"Created {created_count} claims...")
except Exception as e:
self.stdout.write(
self.style.ERROR(f"Error creating claim: {str(e)}")
)
continue
# Show results
self.stdout.write(
self.style.SUCCESS(f"Successfully created {created_count} insurance claims")
)
# Show statistics
if created_claims:
stats = generator.get_saudi_insurance_statistics(claims_data)
self.stdout.write(f"\nGenerated claims statistics:")
self.stdout.write(f" - Total claims: {stats['total_claims']}")
self.stdout.write(f" - Approved claims: {stats['approved_claims']}")
self.stdout.write(f" - Denied claims: {stats['denied_claims']}")
self.stdout.write(f" - Pending claims: {stats['pending_claims']}")
self.stdout.write(f" - Approval rate: {stats['approval_rate']:.1f}%")
self.stdout.write(f" - Total billed: {stats['total_billed_sar']:,.2f} SAR")
self.stdout.write(f" - Total approved: {stats['total_approved_sar']:,.2f} SAR")
self.stdout.write(f" - Average claim: {stats['average_claim_amount_sar']:,.2f} SAR")
# Show sample claims
if self.verbose and created_claims:
self.stdout.write(f"\nSample created claims:")
for claim in created_claims[:3]:
self.stdout.write(f" - {claim.claim_number}: {claim.patient.get_full_name()} - {claim.billed_amount} SAR ({claim.status})")
def _create_status_history(self, claim, created_by):
"""Create status history for the claim."""
# Create initial status history entry
ClaimStatusHistory.objects.create(
claim=claim,
from_status=None,
to_status='DRAFT',
reason='Initial claim creation',
changed_by=created_by
)
# If claim has progressed beyond draft, create additional history
if claim.status != 'DRAFT':
statuses = ['DRAFT', 'SUBMITTED', 'UNDER_REVIEW']
if claim.status in ['APPROVED', 'PARTIALLY_APPROVED', 'PAID']:
statuses.append('APPROVED')
if claim.status == 'PAID':
statuses.append('PAID')
elif claim.status == 'DENIED':
statuses.append('DENIED')
# Create history entries for each status transition
for i in range(1, len(statuses)):
ClaimStatusHistory.objects.create(
claim=claim,
from_status=statuses[i-1],
to_status=statuses[i],
reason=f'Automatic status progression to {statuses[i]}',
changed_by=created_by
)
def _get_system_user(self):
"""Get or create a system user for created_by field."""
try:
return User.objects.get(username='system')
except User.DoesNotExist:
# Try to get the first superuser
superuser = User.objects.filter(is_superuser=True).first()
if superuser:
return superuser
# Try to get any user
user = User.objects.first()
if user:
return user
# Create a system user if none exists
return User.objects.create_user(
username='system',
email='system@hospital.local',
first_name='System',
last_name='Generator',
is_active=True
)