""" FHIR resource builders — convert ACCESS domain models into FHIR R4 resources. Uses fhirclient's model classes for validated construction; call .as_json() on the result (a plain dict) and hand it to CmsClient for submission. Note that fhirclient's FHIRClient itself is a SMART-on-FHIR authorization client with no HTTP operation support, so transport stays in access_core.services.cms_client. """ import hashlib from django.conf import settings from fhirclient.models.patient import Patient from access_core.intake.models import IntakeRecord CMS_MBI_SYSTEM = "http://terminology.hl7.org/NamingSystem/cmsMBI" def synthetic_mbi(first_name: str, last_name: str, dob) -> str: """ Deterministic synthetic MBI for mock-server testing. PHI-derived pseudonym — only ever sent when ALLOW_SYNTHETIC_MBI is enabled (synthetic-data environments pointed at non-production endpoints). See the gate in _patient_data(). """ raw = f"{first_name}:{last_name}:{dob.isoformat() if dob else ''}" return "S" + hashlib.sha256(raw.encode()).hexdigest()[:10].upper() def _patient_data( first_name: str, last_name: str, middle_name: str | None, dob, sex: str | None, identifiers, ) -> dict: """Shared US Core Patient assembly for intake and beneficiary records.""" patient_data: dict = { "name": [ { "use": "official", "family": last_name, "given": [first_name] + ([middle_name] if middle_name else []), } ], } if dob: patient_data["birthDate"] = dob.isoformat() if sex in ("male", "female", "other", "unknown"): patient_data["gender"] = sex mbi_value = None addresses = [] telecoms = [] for ident in identifiers: meta = ident.identifier_metadata or {} if meta.get("system") == CMS_MBI_SYSTEM: mbi_value = ident.identifier_value elif ident.identifier_type == "address": addresses.append( { "line": [meta.get("line1", ident.identifier_value)], "city": meta.get("city", ""), "state": meta.get("state", ""), "postalCode": meta.get("zip", ""), "country": "US", } ) elif ident.identifier_type in ("email", "phone"): telecoms.append( { "system": ident.identifier_type, "value": ident.identifier_value, } ) if not mbi_value: if settings.ALLOW_SYNTHETIC_MBI: mbi_value = synthetic_mbi(first_name, last_name, dob) else: raise ValueError( "No cmsMBI identifier on the record and ALLOW_SYNTHETIC_MBI is " "disabled — refusing to derive a PHI-based pseudonym. Attach a " "real (or sandbox) cmsMBI identifier, or enable the setting in " "synthetic-data environments." ) patient_data["identifier"] = [{"system": CMS_MBI_SYSTEM, "value": mbi_value}] if addresses: patient_data["address"] = addresses if telecoms: patient_data["telecom"] = telecoms return patient_data class FHIRBuilder: """Builds validated FHIR R4 resources from ACCESS domain models.""" @staticmethod def intake_to_patient(intake_record: IntakeRecord) -> Patient: """ Build a US Core Patient resource from an IntakeRecord. Includes official name, birthDate, administrative gender, MBI identifier (existing cmsMBI identifier if present; otherwise the gated synthetic fallback — see _patient_data()), address and telecom entries derived from intake identifiers. """ return Patient( _patient_data( first_name=intake_record.first_name, last_name=intake_record.last_name, middle_name=intake_record.middle_name or None, dob=intake_record.dob, sex=intake_record.sex, identifiers=intake_record.identifiers.all(), ) ) @staticmethod def beneficiary_to_patient(beneficiary) -> Patient: """ Build a US Core Patient resource from an aligned Beneficiary. Same rules as intake_to_patient; the MBI identifier is taken from a copied cmsMBI identifier when present, otherwise falls back to the deterministic synthetic value (stable across both builders because it derives from name + DOB). """ return Patient( _patient_data( first_name=beneficiary.first_name, last_name=beneficiary.last_name, middle_name=beneficiary.middle_name or None, dob=beneficiary.dob, sex=beneficiary.sex, identifiers=beneficiary.identifiers.all(), ) )