""" Identity & Access — Users, roles, permissions, beneficiaries. Two-stage identity: intake_records -> beneficiaries after CMS eligibility + consent + alignment. """ import uuid from django.db import models from fhirclient.models.patient import Patient from access_core.fields import EncryptedDateField class User(models.Model): """Internal application user (staff, clinicians, reviewers).""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = models.TextField(unique=True) full_name = models.TextField() is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) last_login_at = models.DateTimeField(null=True, blank=True) def __str__(self): return f"{self.full_name} ({self.email})" class Meta: app_label = "identity" ordering = ["created_at"] class Role(models.Model): ROLE_CHOICES = [ ("owner", "Owner"), ("contractor", "Contractor"), ("data_assistant", "Data Assistant"), ("clinical_reviewer", "Clinical Reviewer"), ("physician_reviewer", "Physician Reviewer"), ("offshore_reviewer", "Offshore Reviewer"), ("patient", "Patient"), ("admin", "Admin"), ("technical_tester", "Technical Tester"), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) role_name = models.TextField(choices=ROLE_CHOICES, unique=True) description = models.TextField(null=True, blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.role_name class Meta: app_label = "identity" class UserRole(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_roles") role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name="user_roles") assigned_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name="assignments_made" ) assigned_at = models.DateTimeField(auto_now_add=True) revoked_at = models.DateTimeField(null=True, blank=True) def __str__(self): return f"{self.user} -> {self.role}" class Meta: app_label = "identity" unique_together = ["user", "role"] indexes = [ models.Index(fields=["user", "revoked_at"]), models.Index(fields=["user", "role", "revoked_at"]), ] class RolePermission(models.Model): PERMISSION_CHOICES = [ ("view", "View"), ("create", "Create"), ("edit", "Edit"), ("approve", "Approve"), ("export", "Export"), ("delete", "Delete"), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name="permissions") resource = models.TextField() permission = models.TextField(choices=PERMISSION_CHOICES) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.role}.{self.permission} on {self.resource}" class Meta: app_label = "identity" unique_together = ["role", "resource", "permission"] class AccessGrant(models.Model): GRANT_SCOPE_CHOICES = [ ("global", "Global"), ("beneficiary", "Beneficiary"), ("care_period", "Care Period"), ("task", "Task"), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey( User, on_delete=models.CASCADE, related_name="access_grants" ) beneficiary = models.ForeignKey( "Beneficiary", on_delete=models.CASCADE, null=True, blank=True, related_name="access_grants", ) grant_scope = models.TextField(choices=GRANT_SCOPE_CHOICES) granted_at = models.DateTimeField(auto_now_add=True) revoked_at = models.DateTimeField(null=True, blank=True) def clean(self): from django.core.exceptions import ValidationError if self.grant_scope == "beneficiary" and not self.beneficiary: raise ValidationError( "beneficiary is required when grant_scope is 'beneficiary'" ) def __str__(self): return f"Grant: {self.user} ({self.grant_scope})" class Meta: app_label = "identity" class ExternalSystemIdentifier(models.Model): SYSTEM_CHOICES = [ ("healthie", "Healthie"), ("cms", "CMS"), ("labcorp", "Labcorp"), ("quest", "Quest"), ] ENTITY_CHOICES = [ ("patient", "Patient"), ("account", "Account"), ("form_submission", "Form Submission"), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) beneficiary = models.ForeignKey( "Beneficiary", on_delete=models.CASCADE, related_name="external_ids" ) system_name = models.TextField(choices=SYSTEM_CHOICES) external_id = models.TextField() entity_type = models.TextField(choices=ENTITY_CHOICES) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.system_name}:{self.external_id} ({self.entity_type})" class Meta: app_label = "identity" unique_together = ["beneficiary", "system_name", "entity_type"] class Beneficiary(models.Model): """Promoted from intake_records after CMS eligibility + consent + alignment.""" SEX_CHOICES = [ ("male", "Male"), ("female", "Female"), ("other", "Other"), ("unknown", "Unknown"), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) intake_record = models.ForeignKey( "intake.IntakeRecord", on_delete=models.PROTECT, related_name="promoted_beneficiary", null=True, blank=True, ) first_name = models.TextField() middle_name = models.TextField(null=True, blank=True) last_name = models.TextField() dob = EncryptedDateField() sex = models.TextField(choices=SEX_CHOICES) is_synthetic = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.first_name} {self.last_name} (synthetic={self.is_synthetic})" def to_fhir_patient(self) -> Patient: """Build a validated FHIR R4 Patient resource from this Beneficiary.""" from access_core.services.fhir_builder import FHIRBuilder return FHIRBuilder.beneficiary_to_patient(self) class Meta: app_label = "identity" class BeneficiaryIdentifier(models.Model): IDENTIFIER_TYPE_CHOICES = [ ("email", "Email"), ("phone", "Phone"), ("address", "Address"), ("mbi", "MBI"), ("cms_id", "CMS ID"), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) beneficiary = models.ForeignKey( Beneficiary, on_delete=models.CASCADE, related_name="identifiers" ) identifier_type = models.TextField(choices=IDENTIFIER_TYPE_CHOICES) identifier_value = models.TextField() identifier_metadata = models.JSONField(null=True, blank=True) is_primary = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): from access_core.utils import mask_identifier_value # identifier_value is PHI (MBI/phone/email) — never render it raw. return ( f"{self.beneficiary} - {self.identifier_type}: " f"{mask_identifier_value(self.identifier_value)}" ) class Meta: app_label = "identity" class PatientConsent(models.Model): CONSENT_TYPE_CHOICES = [ ("data_sharing", "Data Sharing"), ("participation", "Participation"), ("withdrawal", "Withdrawal"), ("notice_acknowledgment", "Notice Acknowledgment"), ("cms_data_sharing", "CMS Data Sharing"), ] STATUS_CHOICES = [ ("pending", "Pending"), ("granted", "Granted"), ("declined", "Declined"), ("withdrawn", "Withdrawn"), ] SCOPE_CHOICES = [ ("cms_only", "CMS Only"), ("metaflow_only", "MetaFlow Only"), ("cms_and_metaflow", "CMS and MetaFlow"), ("all_providers", "All Providers"), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) intake_record = models.ForeignKey( "intake.IntakeRecord", on_delete=models.PROTECT, related_name="consents" ) beneficiary = models.ForeignKey( Beneficiary, on_delete=models.SET_NULL, null=True, blank=True, related_name="consents", ) consent_type = models.TextField(choices=CONSENT_TYPE_CHOICES) status = models.TextField(choices=STATUS_CHOICES, default="pending") granted_at = models.DateTimeField(null=True, blank=True) granted_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name="consents_granted", ) withdrawn_at = models.DateTimeField(null=True, blank=True) withdrawal_reason = models.TextField(null=True, blank=True) notice_provided = models.BooleanField(default=False) notice_provided_at = models.DateTimeField(null=True, blank=True) authorization_document = models.ForeignKey( "curation.ClinicalDocument", on_delete=models.SET_NULL, null=True, blank=True ) data_sharing_scope = models.TextField(choices=SCOPE_CHOICES, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Consent: {self.consent_type} ({self.status}) for {self.intake_record}" class Meta: app_label = "identity"