399 lines
16 KiB
Python
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
|
|
)
|
|
|