""" CMS ACCESS API — Alignment service. Builds US Core Patient resource from IntakeRecord/Beneficiary and submits $align (submit-only). The polling worker applies the final result via complete_alignment(), which creates Beneficiary + CarePeriod on success. """ import logging from datetime import datetime, timezone from django.db import transaction from django.utils.timezone import localdate from fhirclient.models.codeableconcept import CodeableConcept from fhirclient.models.coding import Coding from fhirclient.models.parameters import Parameters, ParametersParameter from access_core.cms_integration.models import CMSApiRequest from access_core.cms_integration.services import ( note_failed_attempt, open_target_ids, record_submission, ) from access_core.constants import ACCESSTrack_SYSTEM from access_core.identity.models import ( Beneficiary, BeneficiaryIdentifier, ExternalSystemIdentifier, ) from access_core.intake.models import IntakeRecord from access_core.monitoring.models import Alignment, CarePeriod, ClinicalTrack from access_core.services.cms_client import ( CmsApiError, CmsClient, describe_response_params, get_shared_cms_client, parse_fhir_resource, ) from access_core.services.fhir_builder import FHIRBuilder logger = logging.getLogger(__name__) class CmsAlignmentError(Exception): """Raised on alignment failures.""" def _get_cms_client(): """Process-wide shared CMS client (token + session cached).""" return get_shared_cms_client() def build_alignment_parameters( intake_record, requested_track: str, ) -> Parameters: """ Build FHIR R4 Parameters resource for $align operation. Args: intake_record: IntakeRecord instance with patient data requested_track: Track code (CKM, eCKM, MSK) Returns: Typed fhirclient Parameters resource; serialize with .as_json() before POST. """ patient = FHIRBuilder.intake_to_patient(intake_record) patient_param = ParametersParameter() patient_param.name = "patient" patient_param.resource = patient track_coding = Coding() track_coding.system = ACCESSTrack_SYSTEM track_coding.code = requested_track track_concept = CodeableConcept() track_concept.coding = [track_coding] track_param = ParametersParameter() track_param.name = "track" track_param.valueCodeableConcept = track_concept params = Parameters() params.parameter = [patient_param, track_param] return params def align_beneficiary( intake_record, requested_track: str, ) -> Alignment: """ Submit alignment to CMS ACCESS API (submit-only; no inline polling). Workflow: 1. Validate IntakeRecord has required data 2. Skip if an in-flight submission for this intake is still resumable 3. Build US Core Patient + Parameters resource 4. POST $align operation 5. Record the durable CMSApiSubmissionStatus row and return; cms_polling_worker polls $submission-status and applies the outcome via complete_alignment() (creates Beneficiary + CarePeriod on success) Args: intake_record: IntakeRecord instance (status should be pending_alignment) requested_track: Track code from TRACK_CHOICES (CKM, eCKM, MSK) Returns: Alignment row; beneficiary stays None until the worker applies the result. """ client = _get_cms_client() if not intake_record.first_name or not intake_record.last_name: raise CmsAlignmentError(f"IntakeRecord {intake_record.id} missing name") if not intake_record.dob: raise CmsAlignmentError( f"IntakeRecord {intake_record.id} missing date of birth" ) # Dedupe guard: an in-flight submission for this intake is still resumable # via the polling worker — return its Alignment instead of posting a # duplicate $align. open_ids = open_target_ids(CMSApiRequest.ApiCategory.ALIGNMENT) existing_alignment = Alignment.objects.filter( intake_record=intake_record, id__in=open_ids ).first() if existing_alignment is not None: logger.info( f"Alignment skipped for IntakeRecord {intake_record.id}: in-flight " f"submission on Alignment {existing_alignment.id} still resumable" ) return existing_alignment track, _ = ClinicalTrack.objects.get_or_create( track_name=requested_track, defaults={"is_active": True}, ) now = datetime.now(timezone.utc) alignment = Alignment.objects.create( intake_record=intake_record, clinical_track=track, aligned_at=now, ) logger.info( f"Alignment started for IntakeRecord {intake_record.id} (track={requested_track})" ) request_row = None try: # Build parameters resource (typed fhirclient Parameters) and # serialize once for transport + the audit trail. params = build_alignment_parameters(intake_record, requested_track) payload = params.as_json() # Create the audit row BEFORE posting so a failed request is recorded # too; the status code fills in once the response arrives. request_row = CMSApiRequest.objects.create( api_category="alignment", endpoint="$align", method="POST", request_payload=payload, request_headers={"Content-Type": "application/fhir+json"}, ) submit_response = client.post_operation( resource_type="Patient", operation="$align", parameters=payload, ) request_row.status_code = submit_response.pop("_http_status", 200) request_row.save(update_fields=["status_code"]) submission_id = _extract_submission_id(submit_response) if not submission_id: raise CmsAlignmentError( "Alignment response missing submission-id " + describe_response_params(submit_response) ) logger.info(f"Alignment submitted, ID={submission_id}") # Durable tracking; cms_polling_worker polls $submission-status (each # attempt is logged to the audit trail) and applies the outcome via # complete_alignment(). record_submission( api_category=CMSApiRequest.ApiCategory.ALIGNMENT, endpoint="$align", submission_id=submission_id, target_table="Alignment", target_record_id=alignment.id, ) except Exception as exc: if isinstance(exc, CmsApiError) and request_row is not None: try: note_failed_attempt(request_row, exc) except Exception: # audit recording must not mask the original error logger.warning( "Failed to record failed CMS attempt in the audit trail", exc_info=True, ) logger.error(f"Alignment failed for IntakeRecord {intake_record.id}: {exc}") alignment.aligned_at = datetime.now(timezone.utc) alignment.cms_response = {"error": str(exc)} alignment.save() intake_record.status = "rejected" intake_record.save(update_fields=["status"]) raise CmsAlignmentError(str(exc)) from exc return alignment def complete_alignment( alignment: Alignment, status_response: dict, submission_id: str, cms_request=None, ) -> None: """Apply a final $align outcome to its Alignment row. Called by poll_cms_submissions when the worker receives the result (idempotent: skips if a beneficiary already exists). The completion guard is re-checked under a row lock inside the transaction so two concurrent applies can never both pass it and create duplicate Beneficiary/CarePeriod rows. """ intake_record = alignment.intake_record outcome = _parse_alignment_outcome(status_response) if outcome == "aligned": with transaction.atomic(): # Lock, then re-check on the locked row: a stale in-memory instance # (beneficiary still None) from an overlapping run must not pass. locked = Alignment.objects.select_for_update().get(pk=alignment.pk) if locked.beneficiary is not None: alignment.refresh_from_db() return # already completed by a concurrent apply track = locked.clinical_track beneficiary = _create_beneficiary_from_intake( locked.intake_record, submission_id ) locked.beneficiary = beneficiary locked.cms_response = status_response locked.save() if cms_request is not None: cms_request.beneficiary = beneficiary cms_request.save() CarePeriod.objects.create( beneficiary=beneficiary, clinical_track=track, start_date=localdate(), operational_status="active", ) alignment.refresh_from_db() # keep the caller's instance in sync # Conditional update from the expected pre-state only — a concurrent # transition (e.g. withdrawal) is never clobbered by this stale copy. updated = IntakeRecord.objects.filter( pk=intake_record.pk, status="pending_alignment" ).update(status="active_access_beneficiary") if not updated: logger.warning( f"Alignment completed for Beneficiary {beneficiary.id} but " f"IntakeRecord {intake_record.id} is no longer in " "pending_alignment; status left as-is." ) else: logger.info( f"Alignment completed: IntakeRecord {intake_record.id} -> Beneficiary {beneficiary.id}" ) else: alignment.cms_response = status_response alignment.save() if not Beneficiary.objects.filter(intake_record=intake_record).exists(): IntakeRecord.objects.filter( pk=intake_record.pk, status="pending_alignment" ).update(status="rejected") _extract_submission_id = CmsClient._extract_submission_id def _parse_alignment_outcome(response: dict) -> str: """Parse alignment outcome. Returns 'aligned' or 'failed'. Bodies that validate as FHIR Parameters are walked via the fhirclient model; anything else falls through to raw-dict handling (mock server quirks). """ parsed = parse_fhir_resource(response) if isinstance(parsed, Parameters): for param in parsed.parameter or []: name = (param.name or "").lower() if name == "outcome": for part in param.part or []: code = (part.valueCode or "").lower() value = (part.valueString or "").lower() text = f"{code} {value}" if ( any( kw in text for kw in ("align_success", "aligned", "success") ) and "fail" not in text ): return "aligned" # Nested resource format (mock server): result.resource.parameter[].name=result.valueCodeableConcept.coding[0].code elif name == "result": nested = param.resource if isinstance(nested, Parameters): for np in nested.parameter or []: if (np.name or "") == "result": cc = np.valueCodeableConcept code = ( (cc.coding[0].code or "").lower() if cc and cc.coding else "" ) if any(kw in code for kw in ("aligned", "align_success")): return "aligned" else: # Raw fallback — body did not validate as a FHIR resource. for param in response.get("parameter", []): name = param.get("name", "").lower() if name == "outcome": parts = param.get("part", []) for part in parts: code = (part.get("valueCode") or "").lower() value = (part.get("valueString") or "").lower() text = f"{code} {value}" if ( any( kw in text for kw in ("align_success", "aligned", "success") ) and "fail" not in text ): return "aligned" # Nested resource format (mock server): result.resource.parameter[].name=result.valueCodeableConcept.coding[0].code if name == "result": nested = param.get("resource", {}) for np in nested.get("parameter", []): if np.get("name") == "result": coding = np.get("valueCodeableConcept", {}).get("coding", [{}])[ 0 ] code = (coding.get("code") or "").lower() if any(kw in code for kw in ("aligned", "align_success")): return "aligned" outcome = CmsClient._extract_operation_outcome(response) if outcome: severity = outcome.get("severity", "") if severity == "informational": return "aligned" code = (outcome.get("code") or "").lower() message = (outcome.get("message") or "").lower() if any(kw in message for kw in ("successfully aligned", "alignment complete")): return "aligned" return "failed" def _create_beneficiary_from_intake( intake_record, cms_submission_id: str ) -> Beneficiary: """Promote IntakeRecord to a Beneficiary with CMS identifiers.""" beneficiary = Beneficiary.objects.create( intake_record=intake_record, first_name=intake_record.first_name, middle_name=intake_record.middle_name or None, last_name=intake_record.last_name, dob=intake_record.dob, sex=intake_record.sex, is_synthetic=intake_record.is_synthetic, ) ExternalSystemIdentifier.objects.create( beneficiary=beneficiary, system_name="cms", external_id=cms_submission_id, entity_type="account", ) intake_identifiers = intake_record.identifiers.all() for ident in intake_identifiers: BeneficiaryIdentifier.objects.create( beneficiary=beneficiary, identifier_type=ident.identifier_type if ident.identifier_type in ("email", "phone", "address") else "email", identifier_value=ident.identifier_value, identifier_metadata=ident.identifier_metadata, is_primary=ident.is_primary, ) logger.info( f"Beneficiary {beneficiary.id} created from IntakeRecord {intake_record.id}" ) return beneficiary