""" CMS Integration — API requests/responses, payments, recoupment. """ import uuid from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ # Patient-level FHIR keys redacted from stored CMS payloads when # ALLOW_SYNTHETIC_MBI is disabled. The wire payload sent to CMS is untouched — # only what we persist in audit rows is scrubbed. Identifiers (incl. MBI) are # kept so submissions stay traceable. Scrubbing applies inside Patient # resources only: FHIR Parameters use "name" for parameter labels, which must # survive intact. _PHI_PATIENT_KEYS = ("name", "birthDate", "telecom", "address") def _scrub_phi(value): """Recursively redact PHI fields of any Patient resource in a payload.""" if isinstance(value, dict): scrubbed = {key: _scrub_phi(item) for key, item in value.items()} if scrubbed.get("resourceType") == "Patient": for key in _PHI_PATIENT_KEYS: if key in scrubbed: scrubbed[key] = "[REDACTED]" return scrubbed if isinstance(value, list): return [_scrub_phi(item) for item in value] return value class CMSApiRequest(models.Model): class ApiCategory(models.TextChoices): ELIGIBILITY = "eligibility", _("Eligibility") ALIGNMENT = "alignment", _("Alignment") UNALIGNMENT = "unalignment", _("Unalignment") DATA_REPORTING = "data_reporting", _("Data Reporting") id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) api_category = models.TextField(choices=ApiCategory.choices) endpoint = models.TextField() method = models.TextField() request_payload = models.JSONField() request_headers = models.JSONField() status_code = models.IntegerField(null=True, blank=True) beneficiary = models.ForeignKey( "identity.Beneficiary", on_delete=models.SET_NULL, null=True, blank=True, related_name="cms_requests", ) requested_at = models.DateTimeField(auto_now_add=True) def save(self, *args, **kwargs): if not settings.ALLOW_SYNTHETIC_MBI: self.request_payload = _scrub_phi(self.request_payload) super().save(*args, **kwargs) def __str__(self): return f"{self.api_category}: {self.endpoint} ({self.status_code})" class Meta: app_label = "cms_integration" class CMSApiResponse(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) request = models.ForeignKey( CMSApiRequest, on_delete=models.CASCADE, related_name="responses" ) response_payload = models.JSONField() response_headers = models.JSONField() status_code = models.IntegerField() received_at = models.DateTimeField(auto_now_add=True) def save(self, *args, **kwargs): if not settings.ALLOW_SYNTHETIC_MBI: self.response_payload = _scrub_phi(self.response_payload) super().save(*args, **kwargs) def __str__(self): return f"Response: {self.request} ({self.status_code})" class Meta: app_label = "cms_integration" class CMSApiSubmissionStatus(models.Model): """Durable tracking of a submitted CMS ACCESS operation. Created when an operation POST returns 202 + submission ID (per the CMS Operations Manual v0.9.12, clients must store submission IDs and request context so polling can resume after interruption/timeout). Each poll attempt is logged as its own CMSApiRequest/CMSApiResponse pair; this row records where to look next (next_poll_at) and which business record the final outcome applies to (target_table/target_record_id). """ class Status(models.TextChoices): PENDING = "pending", _("Pending") IN_PROGRESS = "in_progress", _("In Progress") ERROR = "error", _("Error") COMPLETE = "complete", _("Complete") id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) api_category = models.TextField(choices=CMSApiRequest.ApiCategory.choices) source_endpoint = models.TextField( default="", help_text="Operation that produced this submission (e.g. $check-eligibility)", ) status = models.TextField(choices=Status.choices, default=Status.PENDING) submission_id = models.TextField(null=True, blank=True, unique=True) requested_at = models.DateTimeField(auto_now_add=True) next_poll_at = models.DateTimeField(null=True, blank=True) poll_attempts = models.PositiveIntegerField(default=0) target_table = models.TextField( null=True, blank=True, help_text="Model name the final outcome applies to" ) target_record_id = models.UUIDField(null=True, blank=True) def __str__(self): return f"{self.api_category} {self.submission_id}: {self.status}" class Meta: app_label = "cms_integration" class CMSClaimsFile(models.Model): class FileType(models.TextChoices): CLAIM = "claim", _("Claim") PAYMENT = "payment", _("Payment") RECOUPMENT = "recoupment", _("Recoupment") id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) beneficiary = models.ForeignKey( "identity.Beneficiary", on_delete=models.PROTECT, related_name="cms_claims_files", ) file_name = models.TextField() file_reference = models.TextField(null=True, blank=True) file_type = models.TextField(choices=FileType.choices) received_at = models.DateField() processed_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"CMS file: {self.file_name} ({self.file_type})" class Meta: app_label = "cms_integration" class CMSPaymentFile(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) beneficiary = models.ForeignKey( "identity.Beneficiary", on_delete=models.PROTECT, related_name="cms_payment_files", ) care_period = models.ForeignKey( "monitoring.CarePeriod", on_delete=models.SET_NULL, null=True, blank=True ) file_name = models.TextField() file_reference = models.TextField(null=True, blank=True) oap_amount = models.FloatField() payment_date = models.DateField() created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Payment: {self.beneficiary} - ${self.oap_amount}" class Meta: app_label = "cms_integration" class RecoupmentEvent(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) beneficiary = models.ForeignKey( "identity.Beneficiary", on_delete=models.PROTECT, related_name="recoupment_events", ) care_period = models.ForeignKey("monitoring.CarePeriod", on_delete=models.PROTECT) amount = models.FloatField() reason = models.TextField(null=True, blank=True) event_date = models.DateField() status = models.TextField(default="notified") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Recoupment: {self.beneficiary} - ${self.amount} ({self.reason})" class Meta: app_label = "cms_integration"