""" Core app models for hospital management system. Provides foundational infrastructure including tenant management, audit logging, system configuration, and integration utilities. """ import uuid from django.db import models from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from django.utils import timezone from django.core.validators import RegexValidator from django.conf import settings class Tenant(models.Model): """ Multi-tenant support for hospital organizations. Each tenant represents a separate hospital or healthcare organization. """ ORGANIZATION_TYPE_CHOICES = [ ('HOSPITAL', 'Hospital'), ('CLINIC', 'Clinic'), ('HEALTH_SYSTEM', 'Health System'), ('AMBULATORY', 'Ambulatory Care'), ('SPECIALTY', 'Specialty Practice'), ('URGENT_CARE', 'Urgent Care'), ('REHABILITATION', 'Rehabilitation Center'), ('LONG_TERM_CARE', 'Long-term Care'), ] SUBSCRIPTION_PLAN_CHOICES = [ ('BASIC', 'Basic'), ('STANDARD', 'Standard'), ('PREMIUM', 'Premium'), ('ENTERPRISE', 'Enterprise'), ] # Tenant Information tenant_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique tenant identifier' ) name = models.CharField( max_length=200, help_text='Organization name' ) display_name = models.CharField( max_length=200, help_text='Display name for the organization' ) description = models.TextField( blank=True, null=True, help_text='Organization description' ) # Organization Details organization_type = models.CharField( max_length=50, choices=ORGANIZATION_TYPE_CHOICES, default='HOSPITAL' ) # Contact Information address_line1 = models.CharField( max_length=200, help_text='Address line 1' ) address_line2 = models.CharField( max_length=200, blank=True, null=True, help_text='Address line 2' ) city = models.CharField( max_length=100, help_text='City' ) state = models.CharField( max_length=100, help_text='State or province' ) postal_code = models.CharField( max_length=20, help_text='Postal code' ) country = models.CharField( max_length=100, default='Saudi Arabia', help_text='Country' ) # Contact Details phone_number = models.CharField( max_length=20, validators=[RegexValidator( regex=r'^\+?1?\d{9,15}$', message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.' )], help_text='Primary phone number' ) email = models.EmailField( help_text='Primary email address' ) website = models.URLField( blank=True, null=True, help_text='Organization website' ) # Licensing and Accreditation license_number = models.CharField( max_length=100, blank=True, null=True, help_text='Healthcare license number' ) accreditation_body = models.CharField( max_length=100, blank=True, null=True, help_text='Accreditation body (e.g., Joint Commission)' ) accreditation_number = models.CharField( max_length=100, blank=True, null=True, help_text='Accreditation number' ) accreditation_expiry = models.DateField( blank=True, null=True, help_text='Accreditation expiry date' ) # Configuration timezone = models.CharField( max_length=50, default='UTC', help_text='Organization timezone' ) locale = models.CharField( max_length=10, default='en-US', help_text='Organization locale' ) currency = models.CharField( max_length=3, default='SAR', help_text='Organization currency code' ) # Subscription and Billing subscription_plan = models.CharField( max_length=50, choices=SUBSCRIPTION_PLAN_CHOICES, default='BASIC' ) max_users = models.PositiveIntegerField( default=50, help_text='Maximum number of users allowed' ) max_patients = models.PositiveIntegerField( default=1000, help_text='Maximum number of patients allowed' ) # Status is_active = models.BooleanField( default=True, help_text='Tenant is active' ) is_trial = models.BooleanField( default=False, help_text='Tenant is on trial' ) trial_expires_at = models.DateTimeField( blank=True, null=True, help_text='Trial expiration date' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'core_tenant' verbose_name = 'Tenant' verbose_name_plural = 'Tenants' ordering = ['name'] def __str__(self): return self.name @property def is_trial_expired(self): """Check if trial has expired.""" if not self.is_trial or not self.trial_expires_at: return False return timezone.now() > self.trial_expires_at def get_user_count(self): """Get current user count for this tenant.""" User = get_user_model() return User.objects.filter(tenant=self).count() def get_patient_count(self): """Get current patient count for this tenant.""" from patients.models import PatientProfile return PatientProfile.objects.filter(tenant=self).count() class AuditLogEntry(models.Model): """ Comprehensive audit logging for HIPAA/GDPR compliance. Tracks all user actions and system events. """ RISK_LEVEL_CHOICES = [ ('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High'), ('CRITICAL', 'Critical'), ] EVENT_TYPE_CHOICES = [ ('CREATE', 'Create'), ('READ', 'Read'), ('UPDATE', 'Update'), ('DELETE', 'Delete'), ('LOGIN', 'Login'), ('LOGOUT', 'Logout'), ('ACCESS', 'Access'), ('EXPORT', 'Export'), ('PRINT', 'Print'), ('SHARE', 'Share'), ('SYSTEM', 'System Event'), ('ERROR', 'Error'), ('SECURITY', 'Security Event'), ] EVENT_CATEGORY_CHOICES = [ ('AUTHENTICATION', 'Authentication'), ('AUTHORIZATION', 'Authorization'), ('DATA_ACCESS', 'Data Access'), ('DATA_MODIFICATION', 'Data Modification'), ('SYSTEM_ADMINISTRATION', 'System Administration'), ('PATIENT_DATA', 'Patient Data'), ('CLINICAL_DATA', 'Clinical Data'), ('FINANCIAL_DATA', 'Financial Data'), ('SECURITY', 'Security'), ('INTEGRATION', 'Integration'), ('REPORTING', 'Reporting'), ] # Tenant tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, related_name='audit_logs' ) # Log Information log_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique log identifier' ) # Event Information event_type = models.CharField( max_length=50, choices=EVENT_TYPE_CHOICES ) event_category = models.CharField( max_length=50, choices=EVENT_CATEGORY_CHOICES ) # User Information user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='audit_logs' ) user_email = models.EmailField( blank=True, null=True, help_text='User email at time of event' ) user_role = models.CharField( max_length=50, blank=True, null=True, help_text='User role at time of event' ) # Session Information session_key = models.CharField( max_length=40, blank=True, null=True, help_text='Session key' ) ip_address = models.GenericIPAddressField( blank=True, null=True, help_text='IP address' ) user_agent = models.TextField( blank=True, null=True, help_text='User agent string' ) # Object Information content_type = models.ForeignKey( ContentType, on_delete=models.SET_NULL, null=True, blank=True ) object_id = models.PositiveIntegerField( null=True, blank=True ) content_object = GenericForeignKey('content_type', 'object_id') object_repr = models.CharField( max_length=200, blank=True, null=True, help_text='String representation of the object' ) # Event Details action = models.CharField( max_length=200, help_text='Action performed' ) description = models.TextField( help_text='Detailed description of the event' ) # Data Changes changes = models.JSONField( default=dict, help_text='Field changes (before/after values)' ) additional_data = models.JSONField( default=dict, help_text='Additional event data' ) # Patient Context patient_id = models.CharField( max_length=50, blank=True, null=True, help_text='Patient identifier if applicable' ) patient_mrn = models.CharField( max_length=50, blank=True, null=True, help_text='Patient MRN if applicable' ) # Risk Assessment risk_level = models.CharField( max_length=20, choices=RISK_LEVEL_CHOICES, default='LOW' ) # Compliance Flags hipaa_relevant = models.BooleanField( default=False, help_text='Event is HIPAA relevant' ) gdpr_relevant = models.BooleanField( default=False, help_text='Event is GDPR relevant' ) # Status is_successful = models.BooleanField( default=True, help_text='Event was successful' ) error_message = models.TextField( blank=True, null=True, help_text='Error message if event failed' ) # Metadata timestamp = models.DateTimeField( default=timezone.now, help_text='Event timestamp' ) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'core_audit_log_entry' verbose_name = 'Audit Log Entry' verbose_name_plural = 'Audit Log Entries' ordering = ['-timestamp'] indexes = [ models.Index(fields=['tenant', 'event_type', 'timestamp']), models.Index(fields=['user', 'timestamp']), models.Index(fields=['patient_mrn', 'timestamp']), models.Index(fields=['content_type', 'object_id']), models.Index(fields=['risk_level', 'timestamp']), ] def __str__(self): return f"{self.event_type} - {self.action} by {self.user_email or 'System'}" class SystemConfiguration(models.Model): """ System configuration settings for tenant-specific and global configurations. """ DATA_TYPE_CHOICES = [ ('STRING', 'String'), ('INTEGER', 'Integer'), ('FLOAT', 'Float'), ('BOOLEAN', 'Boolean'), ('JSON', 'JSON'), ('DATE', 'Date'), ('DATETIME', 'DateTime'), ] # Tenant (null for global configurations) tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, null=True, blank=True, related_name='configurations' ) # Configuration Information key = models.CharField( max_length=200, help_text='Configuration key' ) value = models.TextField( help_text='Configuration value' ) data_type = models.CharField( max_length=20, choices=DATA_TYPE_CHOICES, default='STRING' ) # Configuration Metadata category = models.CharField( max_length=100, help_text='Configuration category' ) description = models.TextField( blank=True, null=True, help_text='Configuration description' ) # Validation validation_rules = models.JSONField( default=dict, help_text='Validation rules for the configuration value' ) default_value = models.TextField( blank=True, null=True, help_text='Default value' ) # Access Control is_sensitive = models.BooleanField( default=False, help_text='Configuration contains sensitive data' ) is_encrypted = models.BooleanField( default=False, help_text='Configuration value is encrypted' ) required_permission = models.CharField( max_length=100, blank=True, null=True, help_text='Permission required to modify this configuration' ) # Status is_active = models.BooleanField( default=True, help_text='Configuration is active' ) is_readonly = models.BooleanField( default=False, help_text='Configuration is read-only' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='updated_configurations' ) class Meta: db_table = 'core_system_configuration' verbose_name = 'System Configuration' verbose_name_plural = 'System Configurations' unique_together = ['tenant', 'key'] ordering = ['category', 'key'] def __str__(self): tenant_name = self.tenant.name if self.tenant else 'Global' return f"{tenant_name} - {self.key}" def get_typed_value(self): """Get the configuration value converted to its proper type.""" if self.data_type == 'INTEGER': return int(self.value) elif self.data_type == 'FLOAT': return float(self.value) elif self.data_type == 'BOOLEAN': return self.value.lower() in ('true', '1', 'yes', 'on') elif self.data_type == 'JSON': import json return json.loads(self.value) elif self.data_type == 'DATE': from datetime import datetime return datetime.strptime(self.value, '%Y-%m-%d').date() elif self.data_type == 'DATETIME': from datetime import datetime return datetime.fromisoformat(self.value) else: return self.value class SystemNotification(models.Model): """ System-wide notifications and announcements. """ TARGET_AUDIENCE = [ ('ALL_USERS', 'All Users'), ('ADMINISTRATORS', 'Administrators'), ('CLINICAL_STAFF', 'Clinical Staff'), ('SUPPORT_STAFF', 'Support Staff'), ('SPECIFIC_ROLES', 'Specific Roles'), ('SPECIFIC_USERS', 'Specific Users'), ] NOTIFICATION_TYPE_CHOICES = [ ('INFO', 'Information'), ('WARNING', 'Warning'), ('ERROR', 'Error'), ('SUCCESS', 'Success'), ('MAINTENANCE', 'Maintenance'), ('SECURITY', 'Security Alert'), ('FEATURE', 'New Feature'), ('UPDATE', 'System Update'), ] PRIORITY_CHOICES = [ ('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High'), ('URGENT', 'Urgent'), ] # Tenant (null for global notifications) tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, null=True, blank=True, related_name='notifications' ) # Notification Information notification_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique notification identifier' ) title = models.CharField( max_length=200, help_text='Notification title' ) message = models.TextField( help_text='Notification message' ) # Notification Type notification_type = models.CharField( max_length=30, choices=NOTIFICATION_TYPE_CHOICES, ) # Priority priority = models.CharField( max_length=20, choices=PRIORITY_CHOICES, default='MEDIUM' ) # Targeting target_audience = models.CharField( max_length=30, choices=TARGET_AUDIENCE, default='ALL_USERS' ) target_roles = models.JSONField( default=list, help_text='Target user roles (if target_audience is SPECIFIC_ROLES)' ) target_users = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name='targeted_notifications' ) # Display Settings is_dismissible = models.BooleanField( default=True, help_text='Users can dismiss this notification' ) auto_dismiss_after = models.PositiveIntegerField( blank=True, null=True, help_text='Auto-dismiss after X seconds' ) show_on_login = models.BooleanField( default=False, help_text='Show notification on user login' ) # Scheduling start_date = models.DateTimeField( default=timezone.now, help_text='Notification start date' ) end_date = models.DateTimeField( blank=True, null=True, help_text='Notification end date' ) # Actions action_url = models.URLField( blank=True, null=True, help_text='Action URL for the notification' ) action_text = models.CharField( max_length=100, blank=True, null=True, help_text='Action button text' ) # Status is_active = models.BooleanField( default=True, help_text='Notification is active' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name='created_notifications' ) class Meta: db_table = 'core_system_notification' verbose_name = 'System Notification' verbose_name_plural = 'System Notifications' ordering = ['-priority', '-created_at'] def __str__(self): return self.title @property def is_visible(self): """Check if notification should be visible now.""" now = timezone.now() if not self.is_active: return False if now < self.start_date: return False if self.end_date and now > self.end_date: return False return True class IntegrationLog(models.Model): """ Integration logging for external system communications. """ STATUS_CHOICES = [ ('SUCCESS', 'Success'), ('FAILED', 'Failed'), ('PENDING', 'Pending'), ('TIMEOUT', 'Timeout'), ('RETRY', 'Retry'), ] INTEGRATION_TYPE_CHOICES = [ ('HL7', 'HL7 Message'), ('DICOM', 'DICOM Communication'), ('API', 'API Call'), ('DATABASE', 'Database Sync'), ('FILE_TRANSFER', 'File Transfer'), ('WEBHOOK', 'Webhook'), ('EMAIL', 'Email'), ('SMS', 'SMS'), ] # Tenant tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, related_name='integration_logs' ) # Log Information log_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique log identifier' ) # Integration Information integration_type = models.CharField( max_length=30, choices=INTEGRATION_TYPE_CHOICES ) direction = models.CharField( max_length=10, choices=[ ('INBOUND', 'Inbound'), ('OUTBOUND', 'Outbound'), ] ) # External System external_system = models.CharField( max_length=200, help_text='External system name' ) endpoint = models.CharField( max_length=500, blank=True, null=True, help_text='Integration endpoint' ) # Message Information message_type = models.CharField( max_length=100, blank=True, null=True, help_text='Message type (e.g., HL7 message type)' ) message_id = models.CharField( max_length=200, blank=True, null=True, help_text='Message identifier' ) correlation_id = models.UUIDField( blank=True, null=True, help_text='Correlation ID for tracking related messages' ) # Content request_data = models.TextField( blank=True, null=True, help_text='Request data sent' ) response_data = models.TextField( blank=True, null=True, help_text='Response data received' ) # Status status = models.CharField( max_length=20, choices=STATUS_CHOICES, ) # Error Information error_code = models.CharField( max_length=50, blank=True, null=True, help_text='Error code' ) error_message = models.TextField( blank=True, null=True, help_text='Error message' ) # Performance processing_time_ms = models.PositiveIntegerField( blank=True, null=True, help_text='Processing time in milliseconds' ) # Metadata timestamp = models.DateTimeField( default=timezone.now, help_text='Log timestamp' ) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'core_integration_log' verbose_name = 'Integration Log' verbose_name_plural = 'Integration Logs' ordering = ['-timestamp'] indexes = [ models.Index(fields=['tenant', 'integration_type', 'timestamp']), models.Index(fields=['external_system', 'status']), models.Index(fields=['correlation_id']), ] def __str__(self): return f"{self.integration_type} - {self.external_system} - {self.status}" # class Department(models.Model): # """ # Hospital department model for organizational structure. # Represents different departments within a healthcare organization. # """ # # DEPARTMENT_TYPE_CHOICES = [ # ('CLINICAL', 'Clinical Department'), # ('ANCILLARY', 'Ancillary Services'), # ('SUPPORT', 'Support Services'), # ('ADMINISTRATIVE', 'Administrative'), # ('DIAGNOSTIC', 'Diagnostic Services'), # ('THERAPEUTIC', 'Therapeutic Services'), # ('EMERGENCY', 'Emergency Services'), # ('SURGICAL', 'Surgical Services'), # ('MEDICAL', 'Medical Services'), # ('NURSING', 'Nursing Services'), # ('PHARMACY', 'Pharmacy'), # ('LABORATORY', 'Laboratory'), # ('RADIOLOGY', 'Radiology'), # ('REHABILITATION', 'Rehabilitation'), # ('MENTAL_HEALTH', 'Mental Health'), # ('PEDIATRIC', 'Pediatric'), # ('OBSTETRIC', 'Obstetric'), # ('ONCOLOGY', 'Oncology'), # ('CARDIOLOGY', 'Cardiology'), # ('NEUROLOGY', 'Neurology'), # ('ORTHOPEDIC', 'Orthopedic'), # ('OTHER', 'Other'), # ] # # # Tenant relationship # tenant = models.ForeignKey( # Tenant, # on_delete=models.CASCADE, # related_name='core_departments', # help_text='Organization tenant' # ) # # # Department Information # department_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique department identifier' # ) # code = models.CharField( # max_length=20, # help_text='Department code (e.g., CARD, EMER, SURG)' # ) # name = models.CharField( # max_length=100, # help_text='Department name' # ) # description = models.TextField( # blank=True, # null=True, # help_text='Department description' # ) # # # Department Classification # department_type = models.CharField( # max_length=30, # choices=DEPARTMENT_TYPE_CHOICES, # help_text='Type of department' # ) # # # Organizational Structure # parent_department = models.ForeignKey( # 'self', # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='sub_departments', # help_text='Parent department (for hierarchical structure)' # ) # # # Management # department_head = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='headed_departments', # help_text='Department head/manager' # ) # # # Contact Information # phone = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Department phone number' # ) # extension = models.CharField( # max_length=10, # blank=True, # null=True, # help_text='Phone extension' # ) # email = models.EmailField( # blank=True, # null=True, # help_text='Department email' # ) # # # Location # building = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Building name or number' # ) # floor = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Floor number or name' # ) # wing = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Wing or section' # ) # room_numbers = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Room numbers (e.g., 101-110, 201A-205C)' # ) # # # Operational Information # is_active = models.BooleanField( # default=True, # help_text='Department is active' # ) # is_24_hour = models.BooleanField( # default=False, # help_text='Department operates 24 hours' # ) # operating_hours = models.JSONField( # default=dict, # blank=True, # help_text='Operating hours by day of week' # ) # # # Budget and Cost Center # cost_center_code = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Cost center code for financial tracking' # ) # budget_code = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Budget code' # ) # # # Staffing # authorized_positions = models.PositiveIntegerField( # default=0, # help_text='Number of authorized positions' # ) # current_staff_count = models.PositiveIntegerField( # default=0, # help_text='Current number of staff members' # ) # # # Quality and Compliance # accreditation_required = models.BooleanField( # default=False, # help_text='Department requires special accreditation' # ) # accreditation_body = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Accrediting body (e.g., Joint Commission, CAP)' # ) # last_inspection_date = models.DateField( # blank=True, # null=True, # help_text='Last inspection date' # ) # next_inspection_date = models.DateField( # blank=True, # null=True, # help_text='Next scheduled inspection date' # ) # # # Metadata # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # created_by = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='created_departments', # help_text='User who created the department' # ) # # class Meta: # db_table = 'core_department' # verbose_name = 'Department' # verbose_name_plural = 'Departments' # ordering = ['name'] # indexes = [ # models.Index(fields=['tenant', 'department_type']), # models.Index(fields=['code']), # models.Index(fields=['is_active']), # models.Index(fields=['parent_department']), # ] # unique_together = ['tenant', 'code'] # # def __str__(self): # return f"{self.name} ({self.code})" # # @property # def full_name(self): # """Return full department name with parent if applicable""" # if self.parent_department: # return f"{self.parent_department.name} - {self.name}" # return self.name # # @property # def staffing_percentage(self): # """Calculate current staffing percentage""" # if self.authorized_positions > 0: # return (self.current_staff_count / self.authorized_positions) * 100 # return 0 # # def get_all_sub_departments(self): # """Get all sub-departments recursively""" # sub_departments = [] # for sub_dept in self.sub_departments.all(): # sub_departments.append(sub_dept) # sub_departments.extend(sub_dept.get_all_sub_departments()) # return sub_departments