From 86bec3fbaefff8d67590f553c7df07338c162ae4 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Sat, 24 Aug 2024 17:54:47 +0530 Subject: [PATCH 1/9] seperated hcx into a plug --- care/facility/api/serializers/patient.py | 34 - .../management/commands/load_redis_index.py | 2 - care/facility/tasks/redis_index.py | 2 - care/facility/tests/test_pdf_generation.py | 1 - .../utils/reports/discharge_summary.py | 3 - care/hcx/__init__.py | 0 care/hcx/api/serializers/claim.py | 98 -- care/hcx/api/serializers/communication.py | 41 - care/hcx/api/serializers/gateway.py | 38 - care/hcx/api/serializers/policy.py | 67 - care/hcx/api/viewsets/claim.py | 49 - care/hcx/api/viewsets/communication.py | 46 - care/hcx/api/viewsets/gateway.py | 368 ----- care/hcx/api/viewsets/listener.py | 124 -- care/hcx/api/viewsets/policy.py | 44 - care/hcx/apps.py | 7 - care/hcx/migrations/0001_initial_squashed.py | 309 ----- ...im_id_alter_claim_items_alter_policy_id.py | 56 - care/hcx/migrations/0007_communication.py | 100 -- .../migrations/0008_merge_20230617_1253.py | 12 - ...nication_content_alter_communication_id.py | 48 - care/hcx/migrations/__init__.py | 0 care/hcx/migrations_old/0001_initial.py | 116 -- care/hcx/migrations_old/0002_claim.py | 163 --- .../migrations_old/0003_auto_20230217_1901.py | 51 - .../migrations_old/0004_auto_20230222_2012.py | 47 - .../migrations_old/0005_auto_20230222_2217.py | 43 - .../migrations_old/0006_auto_20230323_1208.py | 34 - care/hcx/migrations_old/__init__.py | 0 care/hcx/models/base.py | 62 - care/hcx/models/claim.py | 53 - care/hcx/models/communication.py | 24 - care/hcx/models/json_schema/claim.py | 17 - care/hcx/models/json_schema/communication.py | 15 - care/hcx/models/policy.py | 44 - care/hcx/static_data/__init__.py | 0 care/hcx/static_data/pmjy_packages.py | 46 - care/hcx/utils/fhir.py | 1185 ----------------- care/hcx/utils/hcx/__init__.py | 137 -- care/hcx/utils/hcx/operations.py | 21 - ...patient_discharge_summary_pdf_template.typ | 18 - care/utils/queryset/communications.py | 22 - care/utils/tests/test_utils.py | 26 - config/api_router.py | 10 - config/settings/base.py | 15 - config/urls.py | 27 - plug_config.py | 35 +- 47 files changed, 34 insertions(+), 3626 deletions(-) delete mode 100644 care/hcx/__init__.py delete mode 100644 care/hcx/api/serializers/claim.py delete mode 100644 care/hcx/api/serializers/communication.py delete mode 100644 care/hcx/api/serializers/gateway.py delete mode 100644 care/hcx/api/serializers/policy.py delete mode 100644 care/hcx/api/viewsets/claim.py delete mode 100644 care/hcx/api/viewsets/communication.py delete mode 100644 care/hcx/api/viewsets/gateway.py delete mode 100644 care/hcx/api/viewsets/listener.py delete mode 100644 care/hcx/api/viewsets/policy.py delete mode 100644 care/hcx/apps.py delete mode 100644 care/hcx/migrations/0001_initial_squashed.py delete mode 100644 care/hcx/migrations/0002_alter_claim_id_alter_claim_items_alter_policy_id.py delete mode 100644 care/hcx/migrations/0007_communication.py delete mode 100644 care/hcx/migrations/0008_merge_20230617_1253.py delete mode 100644 care/hcx/migrations/0009_alter_communication_content_alter_communication_id.py delete mode 100644 care/hcx/migrations/__init__.py delete mode 100644 care/hcx/migrations_old/0001_initial.py delete mode 100644 care/hcx/migrations_old/0002_claim.py delete mode 100644 care/hcx/migrations_old/0003_auto_20230217_1901.py delete mode 100644 care/hcx/migrations_old/0004_auto_20230222_2012.py delete mode 100644 care/hcx/migrations_old/0005_auto_20230222_2217.py delete mode 100644 care/hcx/migrations_old/0006_auto_20230323_1208.py delete mode 100644 care/hcx/migrations_old/__init__.py delete mode 100644 care/hcx/models/base.py delete mode 100644 care/hcx/models/claim.py delete mode 100644 care/hcx/models/communication.py delete mode 100644 care/hcx/models/json_schema/claim.py delete mode 100644 care/hcx/models/json_schema/communication.py delete mode 100644 care/hcx/models/policy.py delete mode 100644 care/hcx/static_data/__init__.py delete mode 100644 care/hcx/static_data/pmjy_packages.py delete mode 100644 care/hcx/utils/fhir.py delete mode 100644 care/hcx/utils/hcx/__init__.py delete mode 100644 care/hcx/utils/hcx/operations.py delete mode 100644 care/utils/queryset/communications.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index d46316ae29..1f8d2837cd 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -2,7 +2,6 @@ from django.conf import settings from django.db import transaction -from django.db.models import Q from django.utils.timezone import make_aware, now from rest_framework import serializers @@ -39,8 +38,6 @@ ) from care.facility.models.patient_consultation import PatientConsultation from care.facility.models.patient_external_test import PatientExternalTest -from care.hcx.models.claim import Claim -from care.hcx.models.policy import Policy from care.users.api.serializers.lsg import ( DistrictSerializer, LocalBodySerializer, @@ -84,37 +81,6 @@ class PatientListSerializer(serializers.ModelSerializer): assigned_to_object = UserBaseMinimumSerializer(source="assigned_to", read_only=True) - # HCX - has_eligible_policy = serializers.SerializerMethodField( - "get_has_eligible_policy", read_only=True - ) - - def get_has_eligible_policy(self, patient): - eligible_policies = Policy.objects.filter( - (Q(error_text="") | Q(error_text=None)), - outcome="complete", - patient=patient.id, - ) - return bool(len(eligible_policies)) - - approved_claim_amount = serializers.SerializerMethodField( - "get_approved_claim_amount", read_only=True - ) - - def get_approved_claim_amount(self, patient): - if patient.last_consultation is not None: - claim = ( - Claim.objects.filter( - Q(error_text="") | Q(error_text=None), - consultation__external_id=patient.last_consultation.external_id, - outcome="complete", - total_claim_amount__isnull=False, - ) - .order_by("-modified_date") - .first() - ) - return claim.total_claim_amount if claim is not None else None - class Meta: model = PatientRegistration exclude = ( diff --git a/care/facility/management/commands/load_redis_index.py b/care/facility/management/commands/load_redis_index.py index ed09b4d2ff..243ae95020 100644 --- a/care/facility/management/commands/load_redis_index.py +++ b/care/facility/management/commands/load_redis_index.py @@ -3,7 +3,6 @@ from care.facility.static_data.icd11 import load_icd11_diagnosis from care.facility.static_data.medibase import load_medibase_medicines -from care.hcx.static_data.pmjy_packages import load_pmjy_packages class Command(BaseCommand): @@ -23,6 +22,5 @@ def handle(self, *args, **options): load_icd11_diagnosis() load_medibase_medicines() - load_pmjy_packages() cache.delete("redis_index_loading") diff --git a/care/facility/tasks/redis_index.py b/care/facility/tasks/redis_index.py index 68bb5c6f59..e1281330eb 100644 --- a/care/facility/tasks/redis_index.py +++ b/care/facility/tasks/redis_index.py @@ -6,7 +6,6 @@ from care.facility.static_data.icd11 import load_icd11_diagnosis from care.facility.static_data.medibase import load_medibase_medicines -from care.hcx.static_data.pmjy_packages import load_pmjy_packages from care.utils.static_data.models.base import index_exists logger: Logger = get_task_logger(__name__) @@ -26,7 +25,6 @@ def load_redis_index(): load_icd11_diagnosis() load_medibase_medicines() - load_pmjy_packages() cache.delete("redis_index_loading") logger.info("Redis Index Loaded") diff --git a/care/facility/tests/test_pdf_generation.py b/care/facility/tests/test_pdf_generation.py index 6754c0c43a..e4fc5d2cbc 100644 --- a/care/facility/tests/test_pdf_generation.py +++ b/care/facility/tests/test_pdf_generation.py @@ -143,7 +143,6 @@ def setUpTestData(cls) -> None: suggestion="A", ) cls.create_patient_sample(cls.patient, cls.consultation, cls.facility, cls.user) - cls.create_policy(patient=cls.patient, user=cls.user) cls.create_encounter_symptom(cls.consultation, cls.user) cls.patient_investigation_group = cls.create_patient_investigation_group() cls.patient_investigation = cls.create_patient_investigation( diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index b9fb47d077..dff8a109b8 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -31,7 +31,6 @@ ConditionVerificationStatus, ) from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id -from care.hcx.models.policy import Policy logger = logging.getLogger(__name__) @@ -117,7 +116,6 @@ def get_discharge_summary_data(consultation: PatientConsultation): samples = PatientSample.objects.filter( patient=consultation.patient, consultation=consultation ) - hcx = Policy.objects.filter(patient=consultation.patient) symptoms = EncounterSymptom.objects.filter( consultation=consultation, onset_date__lt=consultation.encounter_date ).exclude(clinical_impression_status=ClinicalImpressionStatus.ENTERED_IN_ERROR) @@ -181,7 +179,6 @@ def get_discharge_summary_data(consultation: PatientConsultation): return { "patient": consultation.patient, "samples": samples, - "hcx": hcx, "symptoms": symptoms, "admitted_to": admitted_to, "admission_duration": admission_duration, diff --git a/care/hcx/__init__.py b/care/hcx/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/hcx/api/serializers/claim.py b/care/hcx/api/serializers/claim.py deleted file mode 100644 index c6466179d2..0000000000 --- a/care/hcx/api/serializers/claim.py +++ /dev/null @@ -1,98 +0,0 @@ -from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ValidationError -from rest_framework.serializers import ( - CharField, - FloatField, - JSONField, - ModelSerializer, - UUIDField, -) - -from care.facility.api.serializers.patient_consultation import ( - PatientConsultationSerializer, -) -from care.facility.models.patient_consultation import PatientConsultation -from care.hcx.api.serializers.policy import PolicySerializer -from care.hcx.models.base import ( - CLAIM_TYPE_CHOICES, - OUTCOME_CHOICES, - PRIORITY_CHOICES, - STATUS_CHOICES, - USE_CHOICES, -) -from care.hcx.models.claim import Claim -from care.hcx.models.json_schema.claim import ITEMS -from care.hcx.models.policy import Policy -from care.users.api.serializers.user import UserBaseMinimumSerializer -from care.utils.models.validators import JSONFieldSchemaValidator -from config.serializers import ChoiceField - -TIMESTAMP_FIELDS = ( - "created_date", - "modified_date", -) - - -class ClaimSerializer(ModelSerializer): - id = UUIDField(source="external_id", read_only=True) - - consultation = UUIDField(write_only=True, required=True) - consultation_object = PatientConsultationSerializer( - source="consultation", read_only=True - ) - - policy = UUIDField(write_only=True, required=True) - policy_object = PolicySerializer(source="policy", read_only=True) - - items = JSONField(required=False, validators=[JSONFieldSchemaValidator(ITEMS)]) - total_claim_amount = FloatField(required=False) - total_amount_approved = FloatField(required=False) - - use = ChoiceField(choices=USE_CHOICES, default="claim") - status = ChoiceField(choices=STATUS_CHOICES, default="active") - priority = ChoiceField(choices=PRIORITY_CHOICES, default="normal") - type = ChoiceField(choices=CLAIM_TYPE_CHOICES, default="institutional") - - outcome = ChoiceField(choices=OUTCOME_CHOICES, read_only=True) - error_text = CharField(read_only=True) - - created_by = UserBaseMinimumSerializer(read_only=True) - last_modified_by = UserBaseMinimumSerializer(read_only=True) - - class Meta: - model = Claim - exclude = ("deleted", "external_id") - read_only_fields = TIMESTAMP_FIELDS - - def validate(self, attrs): - if "consultation" in attrs and "policy" in attrs: - consultation = get_object_or_404( - PatientConsultation.objects.filter(external_id=attrs["consultation"]) - ) - policy = get_object_or_404( - Policy.objects.filter(external_id=attrs["policy"]) - ) - attrs["consultation"] = consultation - attrs["policy"] = policy - else: - raise ValidationError( - {"consultation": "Field is Required", "policy": "Field is Required"} - ) - - if "total_claim_amount" not in attrs and "items" in attrs: - total_claim_amount = 0.0 - for item in attrs["items"]: - total_claim_amount += item["price"] - - attrs["total_claim_amount"] = total_claim_amount - - return super().validate(attrs) - - def create(self, validated_data): - validated_data["created_by"] = self.context["request"].user - validated_data["last_modified_by"] = self.context["request"].user - return super().create(validated_data) - - def update(self, instance, validated_data): - instance.last_modified_by = self.context["request"].user - return super().update(instance, validated_data) diff --git a/care/hcx/api/serializers/communication.py b/care/hcx/api/serializers/communication.py deleted file mode 100644 index 969654c146..0000000000 --- a/care/hcx/api/serializers/communication.py +++ /dev/null @@ -1,41 +0,0 @@ -from rest_framework.serializers import CharField, JSONField, ModelSerializer, UUIDField - -from care.hcx.api.serializers.claim import ClaimSerializer -from care.hcx.models.claim import Claim -from care.hcx.models.communication import Communication -from care.users.api.serializers.user import UserBaseMinimumSerializer -from care.utils.serializer.external_id_field import ExternalIdSerializerField - -TIMESTAMP_FIELDS = ( - "created_date", - "modified_date", -) - - -class CommunicationSerializer(ModelSerializer): - id = UUIDField(source="external_id", read_only=True) - - claim = ExternalIdSerializerField( - queryset=Claim.objects.all(), write_only=True, required=True - ) - claim_object = ClaimSerializer(source="claim", read_only=True) - - identifier = CharField(required=False) - content = JSONField(required=False) - - created_by = UserBaseMinimumSerializer(read_only=True) - last_modified_by = UserBaseMinimumSerializer(read_only=True) - - class Meta: - model = Communication - exclude = ("deleted", "external_id") - read_only_fields = TIMESTAMP_FIELDS - - def create(self, validated_data): - validated_data["created_by"] = self.context["request"].user - validated_data["last_modified_by"] = self.context["request"].user - return super().create(validated_data) - - def update(self, instance, validated_data): - instance.last_modified_by = self.context["request"].user - return super().update(instance, validated_data) diff --git a/care/hcx/api/serializers/gateway.py b/care/hcx/api/serializers/gateway.py deleted file mode 100644 index 8b36efe03b..0000000000 --- a/care/hcx/api/serializers/gateway.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ValidationError -from rest_framework.serializers import Serializer, UUIDField - -from care.hcx.models.claim import Claim -from care.hcx.models.communication import Communication -from care.hcx.models.policy import Policy -from care.utils.serializer.external_id_field import ExternalIdSerializerField - - -class CheckEligibilitySerializer(Serializer): - policy = UUIDField(required=True) - - def validate(self, attrs): - if "policy" in attrs: - get_object_or_404(Policy.objects.filter(external_id=attrs["policy"])) - else: - raise ValidationError({"policy": "Field is Required"}) - - return super().validate(attrs) - - -class MakeClaimSerializer(Serializer): - claim = UUIDField(required=True) - - def validate(self, attrs): - if "claim" in attrs: - get_object_or_404(Claim.objects.filter(external_id=attrs["claim"])) - else: - raise ValidationError({"claim": "Field is Required"}) - - return super().validate(attrs) - - -class SendCommunicationSerializer(Serializer): - communication = ExternalIdSerializerField( - queryset=Communication.objects.all(), required=True - ) diff --git a/care/hcx/api/serializers/policy.py b/care/hcx/api/serializers/policy.py deleted file mode 100644 index 9175399edd..0000000000 --- a/care/hcx/api/serializers/policy.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ValidationError -from rest_framework.serializers import CharField, ModelSerializer, UUIDField - -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.models.patient import PatientRegistration -from care.hcx.models.policy import ( - OUTCOME_CHOICES, - PRIORITY_CHOICES, - PURPOSE_CHOICES, - STATUS_CHOICES, - Policy, -) -from care.users.api.serializers.user import UserBaseMinimumSerializer -from config.serializers import ChoiceField - -TIMESTAMP_FIELDS = ( - "created_date", - "modified_date", -) - - -class PolicySerializer(ModelSerializer): - id = UUIDField(source="external_id", read_only=True) - - patient = UUIDField(write_only=True, required=True) - patient_object = PatientDetailSerializer(source="patient", read_only=True) - - subscriber_id = CharField() - policy_id = CharField() - - insurer_id = CharField(required=False) - insurer_name = CharField(required=False) - - status = ChoiceField(choices=STATUS_CHOICES, default="active") - priority = ChoiceField(choices=PRIORITY_CHOICES, default="normal") - purpose = ChoiceField(choices=PURPOSE_CHOICES, default="benefits") - - outcome = ChoiceField(choices=OUTCOME_CHOICES, read_only=True) - error_text = CharField(read_only=True) - - created_by = UserBaseMinimumSerializer(read_only=True) - last_modified_by = UserBaseMinimumSerializer(read_only=True) - - class Meta: - model = Policy - exclude = ("deleted", "external_id") - read_only_fields = TIMESTAMP_FIELDS - - def validate(self, attrs): - if "patient" in attrs: - patient = get_object_or_404( - PatientRegistration.objects.filter(external_id=attrs["patient"]) - ) - attrs["patient"] = patient - else: - raise ValidationError({"patient": "Field is Required"}) - return super().validate(attrs) - - def create(self, validated_data): - validated_data["created_by"] = self.context["request"].user - validated_data["last_modified_by"] = self.context["request"].user - return super().create(validated_data) - - def update(self, instance, validated_data): - instance.last_modified_by = self.context["request"].user - return super().update(instance, validated_data) diff --git a/care/hcx/api/viewsets/claim.py b/care/hcx/api/viewsets/claim.py deleted file mode 100644 index fe81b615c3..0000000000 --- a/care/hcx/api/viewsets/claim.py +++ /dev/null @@ -1,49 +0,0 @@ -from django_filters import rest_framework as filters -from rest_framework import filters as drf_filters -from rest_framework.mixins import ( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import GenericViewSet - -from care.hcx.api.serializers.claim import ClaimSerializer -from care.hcx.models.base import USE_CHOICES -from care.hcx.models.claim import Claim - - -class PolicyFilter(filters.FilterSet): - consultation = filters.UUIDFilter(field_name="consultation__external_id") - policy = filters.UUIDFilter(field_name="policy__external_id") - use = filters.ChoiceFilter(field_name="use", choices=USE_CHOICES) - - -class ClaimViewSet( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, - GenericViewSet, -): - queryset = Claim.objects.all().select_related( - "policy", "created_by", "last_modified_by" - ) - permission_classes = (IsAuthenticated,) - serializer_class = ClaimSerializer - lookup_field = "external_id" - search_fields = ["consultation", "policy"] - filter_backends = ( - filters.DjangoFilterBackend, - drf_filters.SearchFilter, - drf_filters.OrderingFilter, - ) - filterset_class = PolicyFilter - ordering_fields = [ - "id", - "created_date", - "modified_date", - ] diff --git a/care/hcx/api/viewsets/communication.py b/care/hcx/api/viewsets/communication.py deleted file mode 100644 index 455460794b..0000000000 --- a/care/hcx/api/viewsets/communication.py +++ /dev/null @@ -1,46 +0,0 @@ -from django_filters import rest_framework as filters -from rest_framework import filters as drf_filters -from rest_framework.mixins import ( - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import GenericViewSet - -from care.hcx.api.serializers.communication import CommunicationSerializer -from care.hcx.models.communication import Communication -from care.utils.queryset.communications import get_communications - - -class CommunicationFilter(filters.FilterSet): - claim = filters.UUIDFilter(field_name="claim__external_id") - - -class CommunicationViewSet( - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, - GenericViewSet, -): - queryset = Communication.objects.all() - permission_classes = (IsAuthenticated,) - serializer_class = CommunicationSerializer - lookup_field = "external_id" - search_fields = ["claim"] - filter_backends = ( - filters.DjangoFilterBackend, - drf_filters.SearchFilter, - drf_filters.OrderingFilter, - ) - filterset_class = CommunicationFilter - ordering_fields = [ - "id", - "created_date", - "modified_date", - ] - - def get_queryset(self): - return get_communications(self.request.user) diff --git a/care/hcx/api/viewsets/gateway.py b/care/hcx/api/viewsets/gateway.py deleted file mode 100644 index be621f51bf..0000000000 --- a/care/hcx/api/viewsets/gateway.py +++ /dev/null @@ -1,368 +0,0 @@ -import json -from datetime import datetime -from uuid import uuid4 as uuid - -from django.db.models import Q -from drf_spectacular.utils import extend_schema -from redis_om import FindQuery -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.facility.models.file_upload import FileUpload -from care.facility.models.icd11_diagnosis import ConditionVerificationStatus -from care.facility.models.patient_consultation import PatientConsultation -from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id -from care.facility.utils.reports.discharge_summary import ( - generate_discharge_report_signed_url, -) -from care.hcx.api.serializers.claim import ClaimSerializer -from care.hcx.api.serializers.communication import CommunicationSerializer -from care.hcx.api.serializers.gateway import ( - CheckEligibilitySerializer, - MakeClaimSerializer, - SendCommunicationSerializer, -) -from care.hcx.api.serializers.policy import PolicySerializer -from care.hcx.models.base import ( - REVERSE_CLAIM_TYPE_CHOICES, - REVERSE_PRIORITY_CHOICES, - REVERSE_PURPOSE_CHOICES, - REVERSE_STATUS_CHOICES, - REVERSE_USE_CHOICES, -) -from care.hcx.models.claim import Claim -from care.hcx.models.communication import Communication -from care.hcx.models.policy import Policy -from care.hcx.static_data.pmjy_packages import PMJYPackage -from care.hcx.utils.fhir import Fhir -from care.hcx.utils.hcx import Hcx -from care.hcx.utils.hcx.operations import HcxOperations -from care.utils.queryset.communications import get_communications -from care.utils.static_data.helpers import query_builder - - -class HcxGatewayViewSet(GenericViewSet): - queryset = Policy.objects.all() - permission_classes = (IsAuthenticated,) - - @extend_schema(tags=["hcx"], request=CheckEligibilitySerializer()) - @action(detail=False, methods=["post"]) - def check_eligibility(self, request): - data = request.data - - serializer = CheckEligibilitySerializer(data=data) - serializer.is_valid(raise_exception=True) - - policy = PolicySerializer(self.queryset.get(external_id=data["policy"])).data - - eligibility_check_fhir_bundle = ( - Fhir().create_coverage_eligibility_request_bundle( - policy["id"], - policy["policy_id"], - policy["patient_object"]["facility_object"]["id"], - policy["patient_object"]["facility_object"]["name"], - "IN000018", - "GICOFINDIA", - "GICOFINDIA", - "GICOFINDIA", - policy["last_modified_by"]["username"], - policy["last_modified_by"]["username"], - "223366009", - "7894561232", - policy["patient_object"]["id"], - policy["patient_object"]["name"], - ( - "male" - if policy["patient_object"]["gender"] == 1 - else ( - "female" if policy["patient_object"]["gender"] == 2 else "other" - ) - ), - policy["subscriber_id"], - policy["policy_id"], - policy["id"], - policy["id"], - policy["id"], - policy["patient_object"]["phone_number"], - REVERSE_STATUS_CHOICES[policy["status"]], - REVERSE_PRIORITY_CHOICES[policy["priority"]], - REVERSE_PURPOSE_CHOICES[policy["purpose"]], - ) - ) - - # if not Fhir().validate_fhir_remote(eligibility_check_fhir_bundle.json())[ - # "valid" - # ]: - # return Response( - # {"message": "Invalid FHIR object"}, status=status.HTTP_400_BAD_REQUEST - # ) - - response = Hcx().generateOutgoingHcxCall( - fhirPayload=json.loads(eligibility_check_fhir_bundle.json()), - operation=HcxOperations.COVERAGE_ELIGIBILITY_CHECK, - recipientCode=policy["insurer_id"], - ) - - return Response(dict(response.get("response")), status=status.HTTP_200_OK) - - @extend_schema(tags=["hcx"], request=MakeClaimSerializer()) - @action(detail=False, methods=["post"]) - def make_claim(self, request): - data = request.data - - serializer = MakeClaimSerializer(data=data) - serializer.is_valid(raise_exception=True) - - claim = ClaimSerializer(Claim.objects.get(external_id=data["claim"])).data - consultation = PatientConsultation.objects.get( - external_id=claim["consultation_object"]["id"] - ) - - procedures = [] - if len(consultation.procedure): - procedures = list( - map( - lambda procedure: { - "id": str(uuid()), - "name": procedure["procedure"], - "performed": ( - procedure["time"] - if "time" in procedure - else procedure["frequency"] - ), - "status": ( - ( - "completed" - if datetime.strptime( - procedure["time"], "%Y-%m-%dT%H:%M" - ) - < datetime.now() - else "preparation" - ) - if "time" in procedure - else "in-progress" - ), - }, - consultation.procedure, - ) - ) - - diagnoses = [] - for diagnosis_id, is_principal in consultation.diagnoses.filter( - verification_status=ConditionVerificationStatus.CONFIRMED - ).values_list("diagnosis_id", "is_principal"): - diagnosis = get_icd11_diagnosis_object_by_id(diagnosis_id) - diagnoses.append( - { - "id": str(uuid()), - "label": diagnosis.label.split(" ", 1)[1], - "code": diagnosis.label.split(" ", 1)[0] or "00", - "type": "principal" if is_principal else "clinical", - } - ) - - previous_claim = ( - Claim.objects.filter( - consultation__external_id=claim["consultation_object"]["id"] - ) - .order_by("-modified_date") - .exclude(external_id=claim["id"]) - .first() - ) - related_claims = [] - if previous_claim: - related_claims.append( - {"id": str(previous_claim.external_id), "type": "prior"} - ) - - docs = list( - map( - lambda file: ( - { - "type": "MB", - "name": file.name, - "url": file.read_signed_url(), - } - ), - FileUpload.objects.filter( - Q(associating_id=claim["consultation_object"]["id"]) - | Q(associating_id=claim["id"]) - ), - ) - ) - - if REVERSE_USE_CHOICES[claim["use"]] == "claim": - discharge_summary_url = generate_discharge_report_signed_url( - claim["policy_object"]["patient_object"]["id"] - ) - docs.append( - { - "type": "DIA", - "name": "Discharge Summary", - "url": discharge_summary_url, - } - ) - - claim_fhir_bundle = Fhir().create_claim_bundle( - claim["id"], - claim["id"], - claim["policy_object"]["patient_object"]["facility_object"]["id"], - claim["policy_object"]["patient_object"]["facility_object"]["name"], - "IN000018", - "GICOFINDIA", - "GICOFINDIA", - "GICOFINDIA", - claim["policy_object"]["patient_object"]["id"], - claim["policy_object"]["patient_object"]["name"], - ( - "male" - if claim["policy_object"]["patient_object"]["gender"] == 1 - else ( - "female" - if claim["policy_object"]["patient_object"]["gender"] == 2 - else "other" - ) - ), - claim["policy_object"]["subscriber_id"], - claim["policy_object"]["policy_id"], - claim["policy_object"]["id"], - claim["id"], - claim["id"], - claim["items"], - claim["policy_object"]["patient_object"]["phone_number"], - REVERSE_USE_CHOICES[claim["use"]], - REVERSE_STATUS_CHOICES[claim["status"]], - REVERSE_CLAIM_TYPE_CHOICES[claim["type"]], - REVERSE_PRIORITY_CHOICES[claim["priority"]], - supporting_info=docs, - related_claims=related_claims, - procedures=procedures, - diagnoses=diagnoses, - ) - - # if not Fhir().validate_fhir_remote(claim_fhir_bundle.json())["valid"]: - # return Response( - # {"message": "Invalid FHIR object"}, - # status=status.HTTP_400_BAD_REQUEST, - # ) - - response = Hcx().generateOutgoingHcxCall( - fhirPayload=json.loads(claim_fhir_bundle.json()), - operation=( - HcxOperations.CLAIM_SUBMIT - if REVERSE_USE_CHOICES[claim["use"]] == "claim" - else HcxOperations.PRE_AUTH_SUBMIT - ), - recipientCode=claim["policy_object"]["insurer_id"], - ) - - return Response(dict(response.get("response")), status=status.HTTP_200_OK) - - @extend_schema(tags=["hcx"], request=SendCommunicationSerializer()) - @action(detail=False, methods=["post"]) - def send_communication(self, request): - data = request.data - - serializer = SendCommunicationSerializer(data=data) - serializer.is_valid(raise_exception=True) - - communication = CommunicationSerializer( - get_communications(self.request.user).get(external_id=data["communication"]) - ).data - - payload = [ - *communication["content"], - *list( - map( - lambda file: ( - { - "type": "url", - "name": file.name, - "data": file.read_signed_url(), - } - ), - FileUpload.objects.filter(associating_id=communication["id"]), - ) - ), - ] - - communication_fhir_bundle = Fhir().create_communication_bundle( - communication["id"], - communication["id"], - communication["id"], - communication["id"], - payload, - [{"type": "Claim", "id": communication["claim_object"]["id"]}], - ) - - if not Fhir().validate_fhir_remote(communication_fhir_bundle.json())["valid"]: - return Response( - Fhir().validate_fhir_remote(communication_fhir_bundle.json())["issues"], - status=status.HTTP_400_BAD_REQUEST, - ) - - response = Hcx().generateOutgoingHcxCall( - fhirPayload=json.loads(communication_fhir_bundle.json()), - operation=HcxOperations.COMMUNICATION_ON_REQUEST, - recipientCode=communication["claim_object"]["policy_object"]["insurer_id"], - correlationId=Communication.objects.filter( - claim__external_id=communication["claim_object"]["id"], created_by=None - ) - .last() - .identifier, - ) - - return Response(dict(response.get("response")), status=status.HTTP_200_OK) - - @extend_schema(tags=["hcx"]) - @action(detail=False, methods=["get"]) - def payors(self, request): - payors = Hcx().searchRegistry("roles", "payor")["participants"] - - result = filter(lambda payor: payor["status"] == "Active", payors) - - if query := request.query_params.get("query"): - query = query.lower() - result = filter( - lambda payor: ( - query in payor["participant_name"].lower() - or query in payor["participant_code"].lower() - ), - result, - ) - - response = list( - map( - lambda payor: { - "name": payor["participant_name"], - "code": payor["participant_code"], - }, - result, - ) - ) - - return Response(response, status=status.HTTP_200_OK) - - def serialize_data(self, objects: list[PMJYPackage]): - return [package.get_representation() for package in objects] - - @extend_schema(tags=["hcx"]) - @action(detail=False, methods=["get"]) - def pmjy_packages(self, request): - try: - limit = min(int(request.query_params.get("limit")), 20) - except (ValueError, TypeError): - limit = 20 - - query = [] - if q := request.query_params.get("query"): - query.append(PMJYPackage.vec % query_builder(q)) - - results = FindQuery(expressions=query, model=PMJYPackage, limit=limit).execute( - exhaust_results=False - ) - - return Response(self.serialize_data(results)) diff --git a/care/hcx/api/viewsets/listener.py b/care/hcx/api/viewsets/listener.py deleted file mode 100644 index fd2f6922e3..0000000000 --- a/care/hcx/api/viewsets/listener.py +++ /dev/null @@ -1,124 +0,0 @@ -import json - -from drf_spectacular.utils import extend_schema -from rest_framework import status -from rest_framework.generics import GenericAPIView -from rest_framework.permissions import AllowAny -from rest_framework.response import Response - -from care.hcx.models.claim import Claim -from care.hcx.models.communication import Communication -from care.hcx.models.policy import Policy -from care.hcx.utils.fhir import Fhir -from care.hcx.utils.hcx import Hcx -from care.utils.notification_handler import send_webpush - - -class CoverageElibilityOnCheckView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] - - @extend_schema(tags=["hcx"]) - def post(self, request, *args, **kwargs): - response = Hcx().processIncomingRequest(request.data["payload"]) - data = Fhir().process_coverage_elibility_check_response(response["payload"]) - - policy = Policy.objects.filter(external_id=data["id"]).first() - policy.outcome = data["outcome"] - policy.error_text = data["error"] - policy.save() - - message = { - "type": "MESSAGE", - "from": "coverageelegibility/on_check", - "message": "success" if not data["error"] else "failed", - } - send_webpush( - username=policy.last_modified_by.username, message=json.dumps(message) - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class PreAuthOnSubmitView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] - - @extend_schema(tags=["hcx"]) - def post(self, request, *args, **kwargs): - response = Hcx().processIncomingRequest(request.data["payload"]) - data = Fhir().process_claim_response(response["payload"]) - - claim = Claim.objects.filter(external_id=data["id"]).first() - claim.outcome = data["outcome"] - claim.total_amount_approved = data["total_approved"] - claim.error_text = data["error"] - claim.save() - - message = { - "type": "MESSAGE", - "from": "preauth/on_submit", - "message": "success" if not data["error"] else "failed", - } - send_webpush( - username=claim.last_modified_by.username, message=json.dumps(message) - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class ClaimOnSubmitView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] - - @extend_schema(tags=["hcx"]) - def post(self, request, *args, **kwargs): - response = Hcx().processIncomingRequest(request.data["payload"]) - data = Fhir().process_claim_response(response["payload"]) - - claim = Claim.objects.filter(external_id=data["id"]).first() - claim.outcome = data["outcome"] - claim.total_amount_approved = data["total_approved"] - claim.error_text = data["error"] - claim.save() - - message = { - "type": "MESSAGE", - "from": "preauth/on_submit", - "message": "success" if not data["error"] else "failed", - } - send_webpush( - username=claim.last_modified_by.username, message=json.dumps(message) - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class CommunicationRequestView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] - - @extend_schema(tags=["hcx"]) - def post(self, request, *args, **kwargs): - response = Hcx().processIncomingRequest(request.data["payload"]) - data = Fhir().process_communication_request(response["payload"]) - - claim = Claim.objects.filter(external_id__in=data["about"] or []).last() - communication = Communication.objects.create( - claim=claim, - content=data["payload"], - identifier=data[ - "identifier" - ], # TODO: replace identifier with corelation id - ) - - message = { - "type": "MESSAGE", - "from": "communication/request", - "message": f"{communication.external_id}", - } - send_webpush( - username=claim.last_modified_by.username, message=json.dumps(message) - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/hcx/api/viewsets/policy.py b/care/hcx/api/viewsets/policy.py deleted file mode 100644 index 18ee6abbf5..0000000000 --- a/care/hcx/api/viewsets/policy.py +++ /dev/null @@ -1,44 +0,0 @@ -from django_filters import rest_framework as filters -from rest_framework import filters as drf_filters -from rest_framework.mixins import ( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import GenericViewSet - -from care.hcx.api.serializers.policy import PolicySerializer -from care.hcx.models.policy import Policy - - -class PolicyFilter(filters.FilterSet): - patient = filters.UUIDFilter(field_name="patient__external_id") - - -class PolicyViewSet( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, - GenericViewSet, -): - queryset = Policy.objects.all() - permission_classes = (IsAuthenticated,) - serializer_class = PolicySerializer - lookup_field = "external_id" - search_fields = ["patient"] - filter_backends = ( - filters.DjangoFilterBackend, - drf_filters.SearchFilter, - drf_filters.OrderingFilter, - ) - filterset_class = PolicyFilter - ordering_fields = [ - "id", - "created_date", - "modified_date", - ] diff --git a/care/hcx/apps.py b/care/hcx/apps.py deleted file mode 100644 index 5b4da22bd7..0000000000 --- a/care/hcx/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class HcxConfig(AppConfig): - name = "care.hcx" - verbose_name = _("HCX Integration") diff --git a/care/hcx/migrations/0001_initial_squashed.py b/care/hcx/migrations/0001_initial_squashed.py deleted file mode 100644 index 0ed09f9623..0000000000 --- a/care/hcx/migrations/0001_initial_squashed.py +++ /dev/null @@ -1,309 +0,0 @@ -# Generated by Django 2.2.11 on 2023-06-13 10:52 - -import uuid - -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - initial = True - - replaces = [ - ("hcx", "0001_initial"), - ("hcx", "0002_claim"), - ("hcx", "0003_auto_20230217_1901"), - ("hcx", "0004_auto_20230222_2012"), - ("hcx", "0005_auto_20230222_2217"), - ("hcx", "0006_auto_20230323_1208"), - ] - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="Policy", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("subscriber_id", models.TextField(blank=True, null=True)), - ("policy_id", models.TextField(blank=True, null=True)), - ("insurer_id", models.TextField(blank=True, null=True)), - ("insurer_name", models.TextField(blank=True, null=True)), - ( - "status", - models.CharField( - blank=True, - choices=[ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "priority", - models.CharField( - choices=[ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), - ], - default="normal", - max_length=20, - ), - ), - ( - "purpose", - models.CharField( - blank=True, - choices=[ - ("auth-requirements", "Auth Requirements"), - ("benefits", "Benefits"), - ("discovery", "Discovery"), - ("validation", "Validation"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "outcome", - models.CharField( - blank=True, - choices=[ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ("error_text", models.TextField(blank=True, null=True)), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "last_modified_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="policy_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "patient", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="facility.PatientRegistration", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="Claim", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ( - "items", - django.contrib.postgres.fields.jsonb.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "properties": { - "category": {"type": "string"}, - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ("total_claim_amount", models.FloatField(blank=True, null=True)), - ("total_amount_approved", models.FloatField(blank=True, null=True)), - ( - "use", - models.CharField( - blank=True, - choices=[ - ("claim", "Claim"), - ("preauthorization", "Pre-Authorization"), - ("predetermination", "Pre-Determination"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "status", - models.CharField( - blank=True, - choices=[ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "priority", - models.CharField( - choices=[ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), - ], - default="normal", - max_length=20, - ), - ), - ( - "type", - models.CharField( - blank=True, - choices=[ - ("institutional", "Institutional"), - ("oral", "Oral"), - ("pharmacy", "Pharmacy"), - ("professional", "Professional"), - ("vision", "Vision"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "outcome", - models.CharField( - blank=True, - choices=[ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ("error_text", models.TextField(blank=True, null=True)), - ( - "consultation", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="facility.PatientConsultation", - ), - ), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "last_modified_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="claim_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "policy", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="hcx.Policy" - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/hcx/migrations/0002_alter_claim_id_alter_claim_items_alter_policy_id.py b/care/hcx/migrations/0002_alter_claim_id_alter_claim_items_alter_policy_id.py deleted file mode 100644 index 6f39c8479a..0000000000 --- a/care/hcx/migrations/0002_alter_claim_id_alter_claim_items_alter_policy_id.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 4.2.2 on 2023-06-14 08:36 - -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0001_initial_squashed"), - ] - - operations = [ - migrations.AlterField( - model_name="claim", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - migrations.AlterField( - model_name="claim", - name="items", - field=models.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "properties": { - "category": {"type": "string"}, - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - migrations.AlterField( - model_name="policy", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ] diff --git a/care/hcx/migrations/0007_communication.py b/care/hcx/migrations/0007_communication.py deleted file mode 100644 index ca30d63c5b..0000000000 --- a/care/hcx/migrations/0007_communication.py +++ /dev/null @@ -1,100 +0,0 @@ -# Generated by Django 2.2.11 on 2023-05-10 06:18 - -import uuid - -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("hcx", "0006_auto_20230323_1208"), - ] - - operations = [ - migrations.CreateModel( - name="Communication", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("identifier", models.TextField(blank=True, null=True)), - ( - "content", - django.contrib.postgres.fields.jsonb.JSONField( - default=list, - null=True, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "data": {"type": "string"}, - "type": {"type": "string"}, - }, - "required": ["type", "data"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ( - "claim", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="hcx.Claim" - ), - ), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "last_modified_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="communication_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/hcx/migrations/0008_merge_20230617_1253.py b/care/hcx/migrations/0008_merge_20230617_1253.py deleted file mode 100644 index 587f806bca..0000000000 --- a/care/hcx/migrations/0008_merge_20230617_1253.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.2 on 2023-06-17 07:23 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0002_alter_claim_id_alter_claim_items_alter_policy_id"), - ("hcx", "0007_communication"), - ] - - operations = [] diff --git a/care/hcx/migrations/0009_alter_communication_content_alter_communication_id.py b/care/hcx/migrations/0009_alter_communication_content_alter_communication_id.py deleted file mode 100644 index 0a43c19cf4..0000000000 --- a/care/hcx/migrations/0009_alter_communication_content_alter_communication_id.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.2 on 2023-06-17 07:23 - -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0008_merge_20230617_1253"), - ] - - operations = [ - migrations.AlterField( - model_name="communication", - name="content", - field=models.JSONField( - default=list, - null=True, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "data": {"type": "string"}, - "type": {"type": "string"}, - }, - "required": ["type", "data"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - migrations.AlterField( - model_name="communication", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ] diff --git a/care/hcx/migrations/__init__.py b/care/hcx/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/hcx/migrations_old/0001_initial.py b/care/hcx/migrations_old/0001_initial.py deleted file mode 100644 index e3a7bb854f..0000000000 --- a/care/hcx/migrations_old/0001_initial.py +++ /dev/null @@ -1,116 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-14 07:56 - -import uuid - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("facility", "0335_auto_20230207_1914"), - ] - - operations = [ - migrations.CreateModel( - name="Policy", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("subscriber_id", models.TextField(blank=True, null=True)), - ("policy_id", models.TextField(blank=True, null=True)), - ("insurer_id", models.TextField(blank=True, null=True)), - ("insurer_name", models.TextField(blank=True, null=True)), - ( - "status", - models.CharField( - blank=True, - choices=[ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "priority", - models.CharField( - choices=[ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), - ], - default="normal", - max_length=20, - ), - ), - ( - "purpose", - models.CharField( - blank=True, - choices=[ - ("auth-requirements", "Auth Requirements"), - ("benefits", "Benefits"), - ("discovery", "Discovery"), - ("validation", "Validation"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "outcome", - models.CharField( - blank=True, - choices=[ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ("error_text", models.TextField(blank=True, null=True)), - ( - "patient", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="facility.PatientRegistration", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/hcx/migrations_old/0002_claim.py b/care/hcx/migrations_old/0002_claim.py deleted file mode 100644 index d9efc76a87..0000000000 --- a/care/hcx/migrations_old/0002_claim.py +++ /dev/null @@ -1,163 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-15 08:04 - -import uuid - -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0335_auto_20230207_1914"), - ("hcx", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="Claim", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ( - "procedures", - django.contrib.postgres.fields.jsonb.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ("total_claim_amount", models.FloatField(blank=True, null=True)), - ("total_amount_approved", models.FloatField(blank=True, null=True)), - ( - "use", - models.CharField( - blank=True, - choices=[ - ("claim", "Claim"), - ("preauthorization", "Pre-Authorization"), - ("predetermination", "Pre-Determination"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "status", - models.CharField( - blank=True, - choices=[ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "priority", - models.CharField( - choices=[ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), - ], - default="normal", - max_length=20, - ), - ), - ( - "type", - models.CharField( - blank=True, - choices=[ - ("institutional", "Institutional"), - ("oral", "Oral"), - ("pharmacy", "Pharmacy"), - ("professional", "Professional"), - ("vision", "Vision"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "outcome", - models.CharField( - blank=True, - choices=[ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ("error_text", models.TextField(blank=True, null=True)), - ( - "consultation", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="facility.PatientConsultation", - ), - ), - ( - "policy", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="hcx.Policy" - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/hcx/migrations_old/0003_auto_20230217_1901.py b/care/hcx/migrations_old/0003_auto_20230217_1901.py deleted file mode 100644 index 30fbdade19..0000000000 --- a/care/hcx/migrations_old/0003_auto_20230217_1901.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-17 13:31 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("hcx", "0002_claim"), - ] - - operations = [ - migrations.AddField( - model_name="claim", - name="created_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="claim", - name="last_modified_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="policy", - name="created_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="policy", - name="last_modified_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/care/hcx/migrations_old/0004_auto_20230222_2012.py b/care/hcx/migrations_old/0004_auto_20230222_2012.py deleted file mode 100644 index 55de67d61e..0000000000 --- a/care/hcx/migrations_old/0004_auto_20230222_2012.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-22 14:42 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0003_auto_20230217_1901"), - ] - - operations = [ - migrations.RemoveField( - model_name="claim", - name="procedures", - ), - migrations.AddField( - model_name="claim", - name="items", - field=django.contrib.postgres.fields.jsonb.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "category": "string", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ] diff --git a/care/hcx/migrations_old/0005_auto_20230222_2217.py b/care/hcx/migrations_old/0005_auto_20230222_2217.py deleted file mode 100644 index a8638a2015..0000000000 --- a/care/hcx/migrations_old/0005_auto_20230222_2217.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-22 16:47 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0004_auto_20230222_2012"), - ] - - operations = [ - migrations.AlterField( - model_name="claim", - name="items", - field=django.contrib.postgres.fields.jsonb.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "properties": { - "category": {"type": "string"}, - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ] diff --git a/care/hcx/migrations_old/0006_auto_20230323_1208.py b/care/hcx/migrations_old/0006_auto_20230323_1208.py deleted file mode 100644 index 1b0b8c960f..0000000000 --- a/care/hcx/migrations_old/0006_auto_20230323_1208.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 2.2.11 on 2023-03-23 06:38 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0005_auto_20230222_2217"), - ] - - operations = [ - migrations.AlterField( - model_name="claim", - name="last_modified_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="claim_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AlterField( - model_name="policy", - name="last_modified_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="policy_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/care/hcx/migrations_old/__init__.py b/care/hcx/migrations_old/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/hcx/models/base.py b/care/hcx/models/base.py deleted file mode 100644 index dcc43f1270..0000000000 --- a/care/hcx/models/base.py +++ /dev/null @@ -1,62 +0,0 @@ -def reverse_choices(choices): - output = {} - for choice in choices: - output[choice[1]] = choice[0] - return output - - -# http://hl7.org/fhir/fm-status -STATUS_CHOICES = [ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), -] -REVERSE_STATUS_CHOICES = reverse_choices(STATUS_CHOICES) - - -# http://terminology.hl7.org/CodeSystem/processpriority -PRIORITY_CHOICES = [ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), -] -REVERSE_PRIORITY_CHOICES = reverse_choices(PRIORITY_CHOICES) - - -# http://hl7.org/fhir/eligibilityrequest-purpose -PURPOSE_CHOICES = [ - ("auth-requirements", "Auth Requirements"), - ("benefits", "Benefits"), - ("discovery", "Discovery"), - ("validation", "Validation"), -] -REVERSE_PURPOSE_CHOICES = reverse_choices(PURPOSE_CHOICES) - - -# http://hl7.org/fhir/remittance-outcome -OUTCOME_CHOICES = [ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), -] -REVERSE_OUTCOME_CHOICES = reverse_choices(OUTCOME_CHOICES) - -# http://hl7.org/fhir/claim-use -USE_CHOICES = [ - ("claim", "Claim"), - ("preauthorization", "Pre-Authorization"), - ("predetermination", "Pre-Determination"), -] -REVERSE_USE_CHOICES = reverse_choices(USE_CHOICES) - -# http://hl7.org/fhir/claim-use -CLAIM_TYPE_CHOICES = [ - ("institutional", "Institutional"), - ("oral", "Oral"), - ("pharmacy", "Pharmacy"), - ("professional", "Professional"), - ("vision", "Vision"), -] -REVERSE_CLAIM_TYPE_CHOICES = reverse_choices(CLAIM_TYPE_CHOICES) diff --git a/care/hcx/models/claim.py b/care/hcx/models/claim.py deleted file mode 100644 index 6334e258c0..0000000000 --- a/care/hcx/models/claim.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.db import models -from django.db.models import JSONField - -from care.facility.models.patient import PatientConsultation -from care.hcx.models.base import ( - CLAIM_TYPE_CHOICES, - OUTCOME_CHOICES, - PRIORITY_CHOICES, - STATUS_CHOICES, - USE_CHOICES, -) -from care.hcx.models.json_schema.claim import ITEMS -from care.hcx.models.policy import Policy -from care.users.models import User -from care.utils.models.base import BaseModel -from care.utils.models.validators import JSONFieldSchemaValidator - - -class Claim(BaseModel): - consultation = models.ForeignKey(PatientConsultation, on_delete=models.CASCADE) - policy = models.ForeignKey( - Policy, on_delete=models.CASCADE - ) # cascade - check it with Gigin - - items = JSONField(default=list, validators=[JSONFieldSchemaValidator(ITEMS)]) - total_claim_amount = models.FloatField(blank=True, null=True) - total_amount_approved = models.FloatField(blank=True, null=True) - - use = models.CharField( - choices=USE_CHOICES, max_length=20, default=None, blank=True, null=True - ) - status = models.CharField( - choices=STATUS_CHOICES, max_length=20, default=None, blank=True, null=True - ) - priority = models.CharField( - choices=PRIORITY_CHOICES, max_length=20, default="normal" - ) - type = models.CharField( - choices=CLAIM_TYPE_CHOICES, max_length=20, default=None, blank=True, null=True - ) - - outcome = models.CharField( - choices=OUTCOME_CHOICES, max_length=20, default=None, blank=True, null=True - ) - error_text = models.TextField(null=True, blank=True) - - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) - last_modified_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name="claim_last_modified_by", - ) diff --git a/care/hcx/models/communication.py b/care/hcx/models/communication.py deleted file mode 100644 index c0a8b2e6f2..0000000000 --- a/care/hcx/models/communication.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.db import models - -from care.hcx.models.claim import Claim -from care.hcx.models.json_schema.communication import CONTENT -from care.users.models import User -from care.utils.models.base import BaseModel -from care.utils.models.validators import JSONFieldSchemaValidator - - -class Communication(BaseModel): - identifier = models.TextField(null=True, blank=True) - claim = models.ForeignKey(Claim, on_delete=models.CASCADE) - - content = models.JSONField( - default=list, validators=[JSONFieldSchemaValidator(CONTENT)], null=True - ) - - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) - last_modified_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name="communication_last_modified_by", - ) diff --git a/care/hcx/models/json_schema/claim.py b/care/hcx/models/json_schema/claim.py deleted file mode 100644 index 091a843336..0000000000 --- a/care/hcx/models/json_schema/claim.py +++ /dev/null @@ -1,17 +0,0 @@ -ITEMS = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - "category": {"type": "string"}, - }, - "additionalProperties": False, - "required": ["id", "name", "price"], - } - ], -} diff --git a/care/hcx/models/json_schema/communication.py b/care/hcx/models/json_schema/communication.py deleted file mode 100644 index c120e477ec..0000000000 --- a/care/hcx/models/json_schema/communication.py +++ /dev/null @@ -1,15 +0,0 @@ -CONTENT = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "content": [ - { - "type": "object", - "properties": { - "type": {"type": "string"}, - "data": {"type": "string"}, - }, - "additionalProperties": False, - "required": ["type", "data"], - } - ], -} diff --git a/care/hcx/models/policy.py b/care/hcx/models/policy.py deleted file mode 100644 index 1ee33474cf..0000000000 --- a/care/hcx/models/policy.py +++ /dev/null @@ -1,44 +0,0 @@ -from django.db import models - -from care.facility.models.patient import PatientRegistration -from care.hcx.models.base import ( - OUTCOME_CHOICES, - PRIORITY_CHOICES, - PURPOSE_CHOICES, - STATUS_CHOICES, -) -from care.users.models import User -from care.utils.models.base import BaseModel - - -class Policy(BaseModel): - patient = models.ForeignKey(PatientRegistration, on_delete=models.CASCADE) - - subscriber_id = models.TextField(null=True, blank=True) - policy_id = models.TextField(null=True, blank=True) - - insurer_id = models.TextField(null=True, blank=True) - insurer_name = models.TextField(null=True, blank=True) - - status = models.CharField( - choices=STATUS_CHOICES, max_length=20, default=None, blank=True, null=True - ) - priority = models.CharField( - choices=PRIORITY_CHOICES, max_length=20, default="normal" - ) - purpose = models.CharField( - choices=PURPOSE_CHOICES, max_length=20, default=None, blank=True, null=True - ) - - outcome = models.CharField( - choices=OUTCOME_CHOICES, max_length=20, default=None, blank=True, null=True - ) - error_text = models.TextField(null=True, blank=True) - - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) - last_modified_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name="policy_last_modified_by", - ) diff --git a/care/hcx/static_data/__init__.py b/care/hcx/static_data/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/hcx/static_data/pmjy_packages.py b/care/hcx/static_data/pmjy_packages.py deleted file mode 100644 index be20f9fc60..0000000000 --- a/care/hcx/static_data/pmjy_packages.py +++ /dev/null @@ -1,46 +0,0 @@ -import json -from typing import TypedDict - -from redis_om import Field, Migrator - -from care.utils.static_data.models.base import BaseRedisModel - - -class PMJYPackageObject(TypedDict): - code: str - name: str - price: str - package_name: str - - -class PMJYPackage(BaseRedisModel): - code: str = Field(primary_key=True) - name: str - price: str - package_name: str - vec: str = Field(index=True, full_text_search=True) - - def get_representation(self) -> PMJYPackageObject: - return { - "code": self.code, - "name": self.name, - "price": self.price, - "package_name": self.package_name, - } - - -def load_pmjy_packages(): - print("Loading PMJY Packages into the redis cache...", end="", flush=True) - with open("data/pmjy_packages.json", "r") as f: - pmjy_packages = json.load(f) - for package in pmjy_packages: - PMJYPackage( - code=package["procedure_code"], - name=package["procedure_label"], - price=package["procedure_price"], - package_name=package["package_name"], - vec=f"{package['procedure_label']} {package['package_name']}", - ).save() - - Migrator().run() - print("Done") diff --git a/care/hcx/utils/fhir.py b/care/hcx/utils/fhir.py deleted file mode 100644 index 46ee5326c6..0000000000 --- a/care/hcx/utils/fhir.py +++ /dev/null @@ -1,1185 +0,0 @@ -from datetime import datetime, timezone -from functools import reduce -from typing import List, Literal, TypedDict - -import requests -from fhir.resources import ( - annotation, - attachment, - bundle, - claim, - claimresponse, - codeableconcept, - coding, - communication, - communicationrequest, - condition, - coverage, - coverageeligibilityrequest, - coverageeligibilityresponse, - domainresource, - identifier, - meta, - organization, - patient, - period, - practitionerrole, - procedure, - reference, -) - -from config.settings.base import CURRENT_DOMAIN - - -class PROFILE: - patient = "https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient" - organization = "https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization" - coverage = "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-Coverage.html" - coverage_eligibility_request = "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-CoverageEligibilityRequest.html" - claim = "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-Claim.html" - claim_bundle = ( - "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-ClaimRequestBundle.html" - ) - coverage_eligibility_request_bundle = "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-CoverageEligibilityRequestBundle.html" - practitioner_role = ( - "https://nrces.in/ndhm/fhir/r4/StructureDefinition/PractitionerRole" - ) - procedure = "http://hl7.org/fhir/R4/procedure.html" - condition = "https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition" - communication = ( - "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-Communication.html" - ) - communication_bundle = ( - "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-CommunicationBundle.html" - ) - - -class SYSTEM: - codes = "http://terminology.hl7.org/CodeSystem/v2-0203" - patient_identifier = "http://gicofIndia.com/beneficiaries" - provider_identifier = "http://abdm.gov.in/facilities" - insurer_identifier = "http://irdai.gov.in/insurers" - coverage_identifier = "https://www.gicofIndia.in/policies" - coverage_relationship = ( - "http://terminology.hl7.org/CodeSystem/subscriber-relationship" - ) - priority = "http://terminology.hl7.org/CodeSystem/processpriority" - claim_identifier = CURRENT_DOMAIN - claim_type = "http://terminology.hl7.org/CodeSystem/claim-type" - claim_payee_type = "http://terminology.hl7.org/CodeSystem/payeetype" - claim_item = "https://pmjay.gov.in/hbp-package-code" - claim_bundle_identifier = "https://www.tmh.in/bundle" - coverage_eligibility_request_bundle_identifier = "https://www.tmh.in/bundle" - practitioner_speciality = "http://snomed.info/sct" - claim_supporting_info_category = ( - "http://hcxprotocol.io/codes/claim-supporting-info-categories" - ) - related_claim_relationship = ( - "http://terminology.hl7.org/CodeSystem/ex-relatedclaimrelationship" - ) - procedure_status = "http://hl7.org/fhir/event-status" - condition = "http://snomed.info/sct" - diagnosis_type = "http://terminology.hl7.org/CodeSystem/ex-diagnosistype" - claim_item_category = "https://irdai.gov.in/benefit-billing-group-code" - claim_item_category_pmjy = "https://pmjay.gov.in/benefit-billing-group-code" - communication_identifier = "http://www.providerco.com/communication" - communication_bundle_identifier = "https://www.tmh.in/bundle" - - -PRACTIONER_SPECIALITY = { - "223366009": "Healthcare professional", - "1421009": "Specialized surgeon", - "3430008": "Radiation therapist", - "3842006": "Chiropractor", - "4162009": "Dental assistant", - "5275007": "Auxiliary nurse", - "6816002": "Specialized nurse", - "6868009": "Hospital administrator", - "8724009": "Plastic surgeon", - "11661002": "Neuropathologist", - "11911009": "Nephrologist", - "11935004": "Obstetrician", - "13580004": "School dental assistant", - "14698002": "Medical microbiologist", - "17561000": "Cardiologist", - "18803008": "Dermatologist", - "18850004": "Laboratory hematologist", - "19244007": "Gerodontist", - "20145008": "Removable prosthodontist", - "21365001": "Specialized dentist", - "21450003": "Neuropsychiatrist", - "22515006": "Medical assistant", - "22731001": "Orthopedic surgeon", - "22983004": "Thoracic surgeon", - "23278007": "Community health physician", - "24430003": "Physical medicine specialist", - "24590004": "Urologist", - "25961008": "Electroencephalography specialist", - "26042002": "Dental hygienist", - "26369006": "Public health nurse", - "28229004": "Optometrist", - "28411006": "Neonatologist", - "28544002": "Medical biochemist", - "36682004": "Physiotherapist", - "37154003": "Periodontist", - "37504001": "Orthodontist", - "39677007": "Internal medicine specialist", - "40127002": "Dietitian (general)", - "40204001": "Hematologist", - "40570005": "Interpreter", - "41672002": "Respiratory disease specialist", - "41904004": "Medical X-ray technician", - "43702002": "Occupational health nurse", - "44652006": "Pharmaceutical assistant", - "45419001": "Masseur", - "45440000": "Rheumatologist", - "45544007": "Neurosurgeon", - "45956004": "Sanitarian", - "46255001": "Pharmacist", - "48740002": "Philologist", - "49203003": "Dispensing optician", - "49993003": "Oral surgeon", - "50149000": "Endodontist", - "54503009": "Faith healer", - "56397003": "Neurologist", - "56466003": "Public health physician", - "56542007": "Medical record administrator", - "56545009": "Cardiovascular surgeon", - "57654006": "Fixed prosthodontist", - "59058001": "General physician", - "59169001": "Orthopedic technician", - "59317003": "Dental prosthesis maker and repairer", - "59944000": "Psychologist", - "60008001": "Public health nutritionist", - "61207006": "Medical pathologist", - "61246008": "Laboratory medicine specialist", - "61345009": "Otorhinolaryngologist", - "61894003": "Endocrinologist", - "62247001": "Family medicine specialist", - "63098009": "Clinical immunologist", - "66476003": "Oral pathologist", - "66862007": "Radiologist", - "68867008": "Public health dentist", - "68950000": "Prosthodontist", - "69280009": "Specialized physician", - "71838004": "Gastroenterologist", - "73265009": "Nursing aid", - "75271001": "Professional midwife", - "76166008": "Practical aid (pharmacy)", - "76231001": "Osteopath", - "76899008": "Infectious disease specialist", - "78703002": "General surgeon", - "78729002": "Diagnostic radiologist", - "79898004": "Auxiliary midwife", - "80409005": "Translator", - "80546007": "Occupational therapist", - "80584001": "Psychiatrist", - "80933006": "Nuclear medicine specialist", - "81464008": "Clinical pathologist", - "82296001": "Pediatrician", - "83273008": "Anatomic pathologist", - "83685006": "Gynecologist", - "85733003": "General pathologist", - "88189002": "Anesthesiologist", - "90201008": "Pedodontist", - "90655003": "Geriatrics specialist", - "106289002": "Dentist", - "106291005": "Dietician AND/OR public health nutritionist", - "106292003": "Professional nurse", - "106293008": "Nursing personnel", - "106294002": "Midwifery personnel", - "307988006": "Medical technician", - "159036002": "ECG technician", - "159037006": "EEG technician", - "159039009": "AT - Audiology technician", - "159041005": "Trainee medical technician", - "721942007": "Cardiovascular perfusionist (occupation)", - "878786001": "Operating room technician (occupation)", - "878787005": "Anesthesia technician", -} - - -class IClaimItem(TypedDict): - id: str - name: str - price: float - - -class IClaimProcedure(TypedDict): - id: str - name: str - performed: str - status: Literal[ - "preparation", - "in-progress", - "not-done", - "on-hold", - "stopped", - "completed", - "entered-in-error", - "unknown", - ] - - -class IClaimDiagnosis(TypedDict): - id: str - label: str - code: str - type: Literal[ - "admitting", - "clinical", - "differential", - "discharge", - "laboratory", - "nursing", - "prenatal", - "principal", - "radiology", - "remote", - "retrospective", - "self", - ] - - -class IClaimSupportingInfo(TypedDict): - type: str - url: str - name: str - - -class IRelatedClaim(TypedDict): - id: str - type: Literal["prior", "associated"] - - -FHIR_VALIDATION_URL = "https://staging-hcx.swasth.app/hapi-fhir/fhir/Bundle/$validate" - - -class Fhir: - def get_reference_url(self, resource: domainresource.DomainResource): - return f"{resource.resource_type}/{resource.id}" - - def create_patient_profile( - self, id: str, name: str, gender: str, phone: str, identifier_value: str - ): - return patient.Patient( - id=id, - meta=meta.Meta(profile=[PROFILE.patient]), - identifier=[ - identifier.Identifier( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.codes, - code="SN", - display="Subscriber Number", - ) - ] - ), - system=SYSTEM.patient_identifier, - value=identifier_value, - ) - ], - name=[{"text": name}], - gender=gender, - telecom=[{"system": "phone", "use": "mobile", "value": phone}], - ) - - def create_provider_profile(self, id: str, name: str, identifier_value: str): - return organization.Organization( - id=id, - meta=meta.Meta(profile=[PROFILE.organization]), - identifier=[ - identifier.Identifier( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.codes, - code="AC", - display=name, - ) - ] - ), - system=SYSTEM.provider_identifier, - value=identifier_value, - ) - ], - name=name, - ) - - def create_insurer_profile(self, id: str, name: str, identifier_value: str): - return organization.Organization( - id=id, - meta=meta.Meta(profile=[PROFILE.organization]), - identifier=[ - identifier.Identifier( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.codes, - code="AC", - display=name, - ) - ] - ), - system=SYSTEM.insurer_identifier, - value=identifier_value, - ) - ], - name=name, - ) - - def create_practitioner_role_profile( - self, id, identifier_value, speciality: str, phone: str - ): - return practitionerrole.PractitionerRole( - id=id, - meta=meta.Meta(profile=[PROFILE.practitioner_role]), - identifier=[ - identifier.Identifier( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.codes, - code="NP", - display="Nurse practitioner number", - ) - ] - ), - value=identifier_value, - ) - ], - specialty=[ - codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.practitioner_speciality, - code=speciality, - display=PRACTIONER_SPECIALITY[speciality], - ) - ] - ) - ], - telecom=[{"system": "phone", "value": phone}], - ) - - def create_coverage_profile( - self, - id: str, - identifier_value: str, - subscriber_id: str, - patient: patient.Patient, - insurer: organization.Organization, - status="active", - relationship="self", - ): - return coverage.Coverage( - id=id, - meta=meta.Meta(profile=[PROFILE.coverage]), - identifier=[ - identifier.Identifier( - system=SYSTEM.coverage_identifier, value=identifier_value - ) - ], - status=status, - subscriber=reference.Reference(reference=self.get_reference_url(patient)), - subscriberId=subscriber_id, - beneficiary=reference.Reference(reference=self.get_reference_url(patient)), - relationship=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.coverage_relationship, - code=relationship, - ) - ] - ), - payor=[reference.Reference(reference=self.get_reference_url(insurer))], - ) - - def create_coverage_eligibility_request_profile( - self, - id: str, - identifier_value: str, - patient: patient.Patient, - enterer: practitionerrole.PractitionerRole, - provider: organization.Organization, - insurer: organization.Organization, - coverage: coverage.Coverage, - priority="normal", - status="active", - purpose="validation", - service_period_start=datetime.now().astimezone(tz=timezone.utc), - service_period_end=datetime.now().astimezone(tz=timezone.utc), - ): - return coverageeligibilityrequest.CoverageEligibilityRequest( - id=id, - meta=meta.Meta(profile=[PROFILE.coverage_eligibility_request]), - identifier=[identifier.Identifier(value=identifier_value)], - status=status, - priority=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.priority, - code=priority, - ) - ] - ), - purpose=[purpose], - patient=reference.Reference(reference=self.get_reference_url(patient)), - servicedPeriod=period.Period( - start=service_period_start, - end=service_period_end, - ), - created=datetime.now().astimezone(tz=timezone.utc), - enterer=reference.Reference(reference=self.get_reference_url(enterer)), - provider=reference.Reference(reference=self.get_reference_url(provider)), - insurer=reference.Reference(reference=self.get_reference_url(insurer)), - insurance=[ - coverageeligibilityrequest.CoverageEligibilityRequestInsurance( - coverage=reference.Reference( - reference=self.get_reference_url(coverage) - ) - ) - ], - ) - - def create_claim_profile( - self, - id: str, - identifier_value: str, - items: List[IClaimItem], - patient: patient.Patient, - provider: organization.Organization, - insurer: organization.Organization, - coverage: coverage.Coverage, - use="claim", - status="active", - type="institutional", - priority="normal", - claim_payee_type="provider", - supporting_info=[], - related_claims=[], - procedures=[], - diagnoses=[], - ): - return claim.Claim( - id=id, - meta=meta.Meta( - profile=[PROFILE.claim], - ), - identifier=[ - identifier.Identifier( - system=SYSTEM.claim_identifier, value=identifier_value - ) - ], - status=status, - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.claim_type, - code=type, - ) - ] - ), - use=use, - related=list( - map( - lambda related_claim: ( - claim.ClaimRelated( - id=related_claim["id"], - relationship=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.related_claim_relationship, - code=related_claim["type"], - ) - ] - ), - claim=reference.Reference( - reference=f'Claim/{related_claim["id"]}' - ), - ) - ), - related_claims, - ) - ), - patient=reference.Reference(reference=self.get_reference_url(patient)), - created=datetime.now().astimezone(tz=timezone.utc), - insurer=reference.Reference(reference=self.get_reference_url(insurer)), - provider=reference.Reference(reference=self.get_reference_url(provider)), - priority=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.priority, - code=priority, - ) - ] - ), - payee=claim.ClaimPayee( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.claim_payee_type, - code=claim_payee_type, - ) - ] - ), - party=reference.Reference(reference=self.get_reference_url(provider)), - ), - careTeam=[ - claim.ClaimCareTeam( - sequence=1, - provider=reference.Reference( - reference=self.get_reference_url(provider) - ), - ) - ], - insurance=[ - claim.ClaimInsurance( - sequence=1, - focal=True, - coverage=reference.Reference( - reference=self.get_reference_url(coverage) - ), - ) - ], - item=list( - map( - lambda item, i: ( - claim.ClaimItem( - sequence=i, - productOrService=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.claim_item, - code=item["id"], - display=item["name"], - ) - ] - ), - unitPrice={"value": item["price"], "currency": "INR"}, - category=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=( - SYSTEM.claim_item_category_pmjy - if item["category"] == "HBP" - else SYSTEM.claim_item_category - ), - code=item["category"], - ) - ] - ), - ) - ), - items, - range(1, len(items) + 1), - ) - ), - supportingInfo=list( - map( - lambda info, i: ( - claim.ClaimSupportingInfo( - sequence=i, - category=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.claim_supporting_info_category, - code=info["type"], - ) - ] - ), - valueAttachment=attachment.Attachment( - url=info["url"], - title=info["name"], - ), - ) - ), - supporting_info, - range(1, len(supporting_info) + 1), - ) - ), - procedure=list( - map( - lambda procedure, i: ( - claim.ClaimProcedure( - sequence=i, - procedureReference=reference.Reference( - reference=self.get_reference_url(procedure) - ), - ) - ), - procedures, - range(1, len(procedures) + 1), - ) - ), - diagnosis=list( - map( - lambda diagnosis, i: ( - claim.ClaimDiagnosis( - sequence=i, - diagnosisReference=reference.Reference( - reference=self.get_reference_url(diagnosis["profile"]) - ), - type=[ - codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.diagnosis_type, - code=diagnosis["type"], - ) - ] - ) - ], - ) - ), - diagnoses, - range(1, len(diagnoses) + 1), - ) - ), - ) - - def create_procedure_profile( - self, - id, - name, - patient, - provider, - status="unknown", - performed=None, - ): - return procedure.Procedure( - id=id, - # meta=meta.Meta( - # profile=[PROFILE.procedure], - # ), - status=status, - note=[annotation.Annotation(text=name)], - subject=reference.Reference(reference=self.get_reference_url(patient)), - performer=[ - procedure.ProcedurePerformer( - actor=reference.Reference( - reference=self.get_reference_url(provider) - ) - ) - ], - performedString=performed, - ) - - def create_condition_profile(self, id, code, label, patient): - return condition.Condition( - id=id, - # meta=meta.Meta(profile=[PROFILE.condition]), - code=codeableconcept.CodeableConcept( - coding=[ - coding.Coding(system=SYSTEM.condition, code=code, display=label) - ] - ), - subject=reference.Reference(reference=self.get_reference_url(patient)), - ) - - def create_coverage_eligibility_request_bundle( - self, - id: str, - identifier_value: str, - provider_id: str, - provider_name: str, - provider_identifier_value: str, - insurer_id: str, - insurer_name: str, - insurer_identifier_value: str, - enterer_id: str, - enterer_identifier_value: str, - enterer_speciality: str, - enterer_phone: str, - patient_id: str, - pateint_name: str, - patient_gender: Literal["male", "female", "other", "unknown"], - subscriber_id: str, - policy_id: str, - coverage_id: str, - eligibility_request_id: str, - eligibility_request_identifier_value: str, - patient_phone: str, - status="active", - priority="normal", - purpose="validation", - service_period_start=datetime.now().astimezone(tz=timezone.utc), - service_period_end=datetime.now().astimezone(tz=timezone.utc), - last_upadted=datetime.now().astimezone(tz=timezone.utc), - ): - provider = self.create_provider_profile( - provider_id, provider_name, provider_identifier_value - ) - insurer = self.create_insurer_profile( - insurer_id, insurer_name, insurer_identifier_value - ) - patient = self.create_patient_profile( - patient_id, pateint_name, patient_gender, patient_phone, subscriber_id - ) - enterer = self.create_practitioner_role_profile( - enterer_id, enterer_identifier_value, enterer_speciality, enterer_phone - ) - coverage = self.create_coverage_profile( - coverage_id, - policy_id, - subscriber_id, - patient, - insurer, - status, - ) - coverage_eligibility_request = self.create_coverage_eligibility_request_profile( - eligibility_request_id, - eligibility_request_identifier_value, - patient, - enterer, - provider, - insurer, - coverage, - priority, - status, - purpose, - service_period_start, - service_period_end, - ) - - return bundle.Bundle( - id=id, - meta=meta.Meta( - lastUpdated=last_upadted, - profile=[PROFILE.coverage_eligibility_request_bundle], - ), - identifier=identifier.Identifier( - system=SYSTEM.coverage_eligibility_request_bundle_identifier, - value=identifier_value, - ), - type="collection", - timestamp=datetime.now().astimezone(tz=timezone.utc), - entry=[ - bundle.BundleEntry( - fullUrl=self.get_reference_url(coverage_eligibility_request), - resource=coverage_eligibility_request, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(provider), - resource=provider, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(insurer), - resource=insurer, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(patient), - resource=patient, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(coverage), - resource=coverage, - ), - ], - ) - - def create_claim_bundle( - self, - id: str, - identifier_value: str, - provider_id: str, - provider_name: str, - provider_identifier_value: str, - insurer_id: str, - insurer_name: str, - insurer_identifier_value: str, - patient_id: str, - pateint_name: str, - patient_gender: Literal["male", "female", "other", "unknown"], - subscriber_id: str, - policy_id: str, - coverage_id: str, - claim_id: str, - claim_identifier_value: str, - items: list[IClaimItem], - patient_phone: str, - use="claim", - status="active", - type="institutional", - priority="normal", - claim_payee_type="provider", - last_updated=datetime.now().astimezone(tz=timezone.utc), - supporting_info=[], - related_claims=[], - procedures=[], - diagnoses=[], - ): - provider = self.create_provider_profile( - provider_id, provider_name, provider_identifier_value - ) - insurer = self.create_insurer_profile( - insurer_id, insurer_name, insurer_identifier_value - ) - patient = self.create_patient_profile( - patient_id, pateint_name, patient_gender, patient_phone, subscriber_id - ) - coverage = self.create_coverage_profile( - coverage_id, - policy_id, - subscriber_id, - patient, - insurer, - status, - ) - - procedures = list( - map( - lambda procedure: self.create_procedure_profile( - procedure["id"], - procedure["name"], - patient, - provider, - procedure["status"], - procedure["performed"], - ), - procedures, - ) - ) - - diagnoses = list( - map( - lambda diagnosis: { - "profile": self.create_condition_profile( - diagnosis["id"], diagnosis["code"], diagnosis["label"], patient - ), - "type": diagnosis["type"], - }, - diagnoses, - ) - ) - - claim = self.create_claim_profile( - claim_id, - claim_identifier_value, - items, - patient, - provider, - insurer, - coverage, - use, - status, - type, - priority, - claim_payee_type, - supporting_info=supporting_info, - related_claims=related_claims, - procedures=procedures, - diagnoses=diagnoses, - ) - - return bundle.Bundle( - id=id, - meta=meta.Meta( - lastUpdated=last_updated, - profile=[PROFILE.claim_bundle], - ), - identifier=identifier.Identifier( - system=SYSTEM.claim_bundle_identifier, - value=identifier_value, - ), - type="collection", - timestamp=datetime.now().astimezone(tz=timezone.utc), - entry=[ - bundle.BundleEntry( - fullUrl=self.get_reference_url(claim), - resource=claim, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(provider), - resource=provider, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(insurer), - resource=insurer, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(patient), - resource=patient, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(coverage), - resource=coverage, - ), - *list( - map( - lambda procedure: bundle.BundleEntry( - fullUrl=self.get_reference_url(procedure), - resource=procedure, - ), - procedures, - ) - ), - *list( - map( - lambda diagnosis: bundle.BundleEntry( - fullUrl=self.get_reference_url(diagnosis["profile"]), - resource=diagnosis["profile"], - ), - diagnoses, - ) - ), - ], - ) - - def create_communication_profile( - self, - id: str, - identifier_value: str, - payload: list, - about: list, - last_updated=datetime.now().astimezone(tz=timezone.utc), - ): - return communication.Communication( - id=id, - identifier=[ - identifier.Identifier( - system=SYSTEM.communication_identifier, value=identifier_value - ) - ], - meta=meta.Meta(lastUpdated=last_updated, profile=[PROFILE.communication]), - status="completed", - about=list( - map( - lambda ref: (reference.Reference(type=ref["type"], id=ref["id"])), - about, - ) - ), - payload=list( - map( - lambda content: ( - communication.CommunicationPayload( - contentString=( - content["data"] if content["type"] == "text" else None - ), - contentAttachment=( - attachment.Attachment( - url=content["data"], - title=content["name"] if content["name"] else None, - ) - if content["type"] == "url" - else None - ), - ) - ), - payload, - ) - ), - ) - - def create_communication_bundle( - self, - id: str, - identifier_value: str, - communication_id: str, - communication_identifier_value: str, - payload: list, - about: list, - last_updated=datetime.now().astimezone(tz=timezone.utc), - ): - communication_profile = self.create_communication_profile( - communication_id, - communication_identifier_value, - payload, - about, - last_updated, - ) - - return bundle.Bundle( - id=id, - meta=meta.Meta( - lastUpdated=last_updated, - profile=[PROFILE.communication_bundle], - ), - identifier=identifier.Identifier( - system=SYSTEM.communication_bundle_identifier, - value=identifier_value, - ), - type="collection", - timestamp=datetime.now().astimezone(tz=timezone.utc), - entry=[ - bundle.BundleEntry( - fullUrl=self.get_reference_url(communication_profile), - resource=communication_profile, - ), - ], - ) - - def process_coverage_elibility_check_response(self, response): - coverage_eligibility_check_bundle = bundle.Bundle(**response) - - coverage_eligibility_check_response = ( - coverageeligibilityresponse.CoverageEligibilityResponse( - **list( - filter( - lambda entry: entry.resource - is coverageeligibilityresponse.CoverageEligibilityResponse, - coverage_eligibility_check_bundle.entry, - ) - )[0].resource.dict() - ) - ) - coverage_request = coverage.Coverage( - **list( - filter( - lambda entry: entry.resource is coverage.Coverage, - coverage_eligibility_check_bundle.entry, - ) - )[0].resource.dict() - ) - - def get_errors_from_coding(codings): - return "; ".join( - list(map(lambda coding: f"{coding.code}: {coding.display}", codings)) - ) - - return { - "id": coverage_request.id, - "outcome": coverage_eligibility_check_response.outcome, - "error": ", ".join( - list( - map( - lambda error: get_errors_from_coding(error.code.coding), - coverage_eligibility_check_response.error or [], - ) - ) - ), - } - - def process_claim_response(self, response): - claim_bundle = bundle.Bundle(**response) - - claim_response = claimresponse.ClaimResponse( - **list( - filter( - lambda entry: entry.resource is claimresponse.ClaimResponse, - claim_bundle.entry, - ) - )[0].resource.dict() - ) - - def get_errors_from_coding(codings): - return "; ".join( - list(map(lambda coding: f"{coding.code}: {coding.display}", codings)) - ) - - return { - "id": claim_bundle.id, - "total_approved": reduce( - lambda price, acc: price + acc, - map( - lambda claim_response_total: float( - claim_response_total.amount.value - ), - claim_response.total, - ), - 0.0, - ), - "outcome": claim_response.outcome, - "error": ", ".join( - list( - map( - lambda error: get_errors_from_coding(error.code.coding), - claim_response.error or [], - ) - ) - ), - } - - def process_communication_request(self, request): - communication_request = communicationrequest.CommunicationRequest(**request) - - data = { - "identifier": communication_request.id - or communication_request.identifier[0].value, - "status": communication_request.status, - "priority": communication_request.priority, - "about": None, - "based_on": None, - "payload": None, - } - - if communication_request.about: - data["about"] = [] - for object in communication_request.about: - about = reference.Reference(**object.dict()) - if about.identifier: - id = identifier.Identifier(about.identifier).value - data["about"].append(id) - continue - - if about.reference: - id = about.reference.split("/")[-1] - data["about"].append(id) - continue - - if communication_request.basedOn: - data["based_on"] = [] - for object in communication_request.basedOn: - based_on = reference.Reference(**object.dict()) - if based_on.identifier: - id = identifier.Identifier(based_on.identifier).value - data["based_on"].append(id) - continue - - if based_on.reference: - id = based_on.reference.split("/")[-1] - data["based_on"].append(id) - continue - - if communication_request.payload: - data["payload"] = [] - for object in communication_request.payload: - payload = communicationrequest.CommunicationRequestPayload( - **object.dict() - ) - - if payload.contentString: - data["payload"].append( - {"type": "text", "data": payload.contentString} - ) - continue - - if payload.contentAttachment: - content = attachment.Attachment(payload.contentAttachment) - if content.data: - data["payload"].append( - { - "type": content.contentType or "text", - "data": content.data, - } - ) - elif content.url: - data["payload"].append({"type": "url", "data": content.url}) - - return data - - def validate_fhir_local(self, fhir_payload, type="bundle"): - try: - if type == "bundle": - bundle.Bundle(**fhir_payload) - except Exception as e: - return {"valid": False, "issues": [e]} - - return {"valid": True, "issues": None} - - def validate_fhir_remote(self, fhir_payload): - headers = {"Content-Type": "application/json"} - response = requests.request( - "POST", FHIR_VALIDATION_URL, headers=headers, data=fhir_payload - ).json() - - issues = response["issue"] if "issue" in response else [] - valid = True - - for issue in issues: - if issue["severity"] == "error": - valid = False - break - - return {"valid": valid, "issues": issues} diff --git a/care/hcx/utils/hcx/__init__.py b/care/hcx/utils/hcx/__init__.py deleted file mode 100644 index 533e0a9bbc..0000000000 --- a/care/hcx/utils/hcx/__init__.py +++ /dev/null @@ -1,137 +0,0 @@ -import datetime -import json -import uuid -from urllib.parse import urlencode - -import requests -from django.conf import settings -from jwcrypto import jwe, jwk - - -class Hcx: - def __init__( - self, - protocolBasePath=settings.HCX_PROTOCOL_BASE_PATH, - participantCode=settings.HCX_PARTICIPANT_CODE, - authBasePath=settings.HCX_AUTH_BASE_PATH, - username=settings.HCX_USERNAME, - password=settings.HCX_PASSWORD, - encryptionPrivateKeyURL=settings.HCX_ENCRYPTION_PRIVATE_KEY_URL, - igUrl=settings.HCX_IG_URL, - ): - self.protocolBasePath = protocolBasePath - self.participantCode = participantCode - self.authBasePath = authBasePath - self.username = username - self.password = password - self.encryptionPrivateKeyURL = encryptionPrivateKeyURL - self.igUrl = igUrl - - def generateHcxToken(self): - url = self.authBasePath - - payload = { - "client_id": "registry-frontend", - "username": self.username, - "password": self.password, - "grant_type": "password", - } - payload_urlencoded = urlencode(payload) - headers = {"content-type": "application/x-www-form-urlencoded"} - - response = requests.request( - "POST", url, headers=headers, data=payload_urlencoded - ) - y = json.loads(response.text) - return y["access_token"] - - def searchRegistry(self, searchField, searchValue): - url = self.protocolBasePath + "/participant/search" - access_token = self.generateHcxToken() - payload = json.dumps({"filters": {searchField: {"eq": searchValue}}}) - headers = { - "Authorization": "Bearer " + access_token, - "Content-Type": "application/json", - } - - response = requests.request("POST", url, headers=headers, data=payload) - return dict(json.loads(response.text)) - - def createHeaders(self, recipientCode=None, correlationId=None): - # creating HCX headers - # getting sender code - # regsitry_user = self.searchRegistry("primary_email", self.username) - hcx_headers = { - "alg": "RSA-OAEP", - "enc": "A256GCM", - "x-hcx-recipient_code": recipientCode, - "x-hcx-timestamp": datetime.datetime.now() - .astimezone() - .replace(microsecond=0) - .isoformat(), - "x-hcx-sender_code": self.participantCode, - "x-hcx-correlation_id": correlationId - if correlationId - else str(uuid.uuid4()), - # "x-hcx-workflow_id": str(uuid.uuid4()), - "x-hcx-api_call_id": str(uuid.uuid4()), - # "x-hcx-status": "response.complete", - } - return hcx_headers - - def encryptJWE(self, recipientCode=None, fhirPayload=None, correlationId=None): - if recipientCode is None: - raise ValueError("Recipient code can not be empty, must be a string") - if type(fhirPayload) is not dict: - raise ValueError("Fhir paylaod must be a dictionary") - regsitry_data = self.searchRegistry( - searchField="participant_code", searchValue=recipientCode - ) - public_cert = requests.get(regsitry_data["participants"][0]["encryption_cert"]) - key = jwk.JWK.from_pem(public_cert.text.encode("utf-8")) - headers = self.createHeaders(recipientCode, correlationId) - jwePayload = jwe.JWE( - str(json.dumps(fhirPayload)), - recipient=key, - protected=json.dumps(headers), - ) - enc = jwePayload.serialize(compact=True) - return enc - - def decryptJWE(self, encryptedString): - private_key = requests.get(self.encryptionPrivateKeyURL) - privateKey = jwk.JWK.from_pem(private_key.text.encode("utf-8")) - jwetoken = jwe.JWE() - jwetoken.deserialize(encryptedString, key=privateKey) - return { - "headers": dict(json.loads(jwetoken.payload.decode("utf-8"))), - "payload": dict(json.loads(jwetoken.payload.decode("utf-8"))), - } - - def makeHcxApiCall(self, operation, encryptedString): - url = "".join(self.protocolBasePath + operation.value) - print("making the API call to url " + url) - access_token = self.generateHcxToken() - payload = json.dumps({"payload": encryptedString}) - headers = { - "Authorization": "Bearer " + access_token, - "Content-Type": "application/json", - } - response = requests.request("POST", url, headers=headers, data=payload) - return dict(json.loads(response.text)) - - def generateOutgoingHcxCall( - self, fhirPayload, operation, recipientCode, correlationId=None - ): - encryptedString = self.encryptJWE( - recipientCode=recipientCode, - fhirPayload=fhirPayload, - correlationId=correlationId, - ) - response = self.makeHcxApiCall( - operation=operation, encryptedString=encryptedString - ) - return {"payload": encryptedString, "response": response} - - def processIncomingRequest(self, encryptedString): - return self.decryptJWE(encryptedString=encryptedString) diff --git a/care/hcx/utils/hcx/operations.py b/care/hcx/utils/hcx/operations.py deleted file mode 100644 index 8e2615064a..0000000000 --- a/care/hcx/utils/hcx/operations.py +++ /dev/null @@ -1,21 +0,0 @@ -import enum - - -class HcxOperations(enum.Enum): - COVERAGE_ELIGIBILITY_CHECK = "/coverageeligibility/check" - COVERAGE_ELIGIBILITY_ON_CHECK = "/coverageeligibility/on_check" - PRE_AUTH_SUBMIT = "/preauth/submit" - PRE_AUTH_ON_SUBMIT = "/preauth/on_submit" - CLAIM_SUBMIT = "/claim/submit" - CLAIM_ON_SUBMIT = "/claim/on_submit" - PAYMENT_NOTICE_REQUEST = "/paymentnotice/request" - PAYMENT_NOTICE_ON_REQUEST = "/paymentnotice/on_request" - HCX_STATUS = "/hcx/status" - HCX_ON_STATUS = "/hcx/on_status" - COMMUNICATION_REQUEST = "/communication/request" - COMMUNICATION_ON_REQUEST = "/communication/on_request" - PREDETERMINATION_SUBMIT = "/predetermination/submit" - PREDETERMINATION_ON_SUBMIT = "/predetermination/on_submit" - - def __str__(self): - return "%s" % self.value diff --git a/care/templates/reports/patient_discharge_summary_pdf_template.typ b/care/templates/reports/patient_discharge_summary_pdf_template.typ index b24c8402e4..c24079b6dc 100644 --- a/care/templates/reports/patient_discharge_summary_pdf_template.typ +++ b/care/templates/reports/patient_discharge_summary_pdf_template.typ @@ -380,24 +380,6 @@ - {% endif %}]] -{% if hcx %} - #align(center, [#line(length: 40%, stroke: mygray,)]) - - #align(left, text(14pt,weight: "bold")[=== Health Insurance Details]) - - #table( - columns: (1fr, 1fr, 1fr, 1fr), - inset: 10pt, - align: horizon, - table.header( - [*INSURER NAME*], [*ISSUER ID*], [*MEMBER ID*], [*POLICY ID*], - ), - {% for policy in hcx %} - "{{policy.insurer_name|format_empty_data }}", "{{policy.insurer_id|format_empty_data }}", "{{policy.subscriber_id }}", "{{policy.policy_id }}", - {% endfor %} - ) -{% endif %} - {% if files %} #align(center, [#line(length: 40%, stroke: mygray,)]) diff --git a/care/utils/queryset/communications.py b/care/utils/queryset/communications.py deleted file mode 100644 index 81ea679f8c..0000000000 --- a/care/utils/queryset/communications.py +++ /dev/null @@ -1,22 +0,0 @@ -from care.hcx.models.communication import Communication -from care.users.models import User -from care.utils.cache.cache_allowed_facilities import get_accessible_facilities - - -def get_communications(user): - queryset = Communication.objects.all() - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(claim__policy__patient__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter( - claim__policy__patient__facility__district=user.district - ) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter( - claim__policy__patient__facility__id__in=allowed_facilities - ) - - return queryset diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 3b340ff5b0..893ea70c93 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -51,7 +51,6 @@ PatientCodeStatusType, PatientConsent, ) -from care.hcx.models.policy import Policy from care.users.models import District, State fake = Faker() @@ -568,31 +567,6 @@ def create_patient_sample( sample.save() return sample - @classmethod - def get_policy_data(cls, patient, user) -> dict: - return { - "patient": patient, - "subscriber_id": "sample_subscriber_id", - "policy_id": "sample_policy_id", - "insurer_id": "sample_insurer_id", - "insurer_name": "Sample Insurer", - "status": "active", - "priority": "normal", - "purpose": "discovery", - "outcome": "complete", - "error_text": "No errors", - "created_by": user, - "last_modified_by": user, - } - - @classmethod - def create_policy( - cls, patient: PatientRegistration, user: User, **kwargs - ) -> Policy: - data = cls.get_policy_data(patient, user) - data.update(**kwargs) - return Policy.objects.create(**data) - @classmethod def get_encounter_symptom_data(cls, consultation, user) -> dict: return { diff --git a/config/api_router.py b/config/api_router.py index 78cb45736b..b71f977c17 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -90,10 +90,6 @@ TestsSummaryViewSet, TriageSummaryViewSet, ) -from care.hcx.api.viewsets.claim import ClaimViewSet -from care.hcx.api.viewsets.communication import CommunicationViewSet -from care.hcx.api.viewsets.gateway import HcxGatewayViewSet -from care.hcx.api.viewsets.policy import PolicyViewSet from care.users.api.viewsets.lsg import ( DistrictViewSet, LocalBodyViewSet, @@ -300,12 +296,6 @@ router.register("medibase", MedibaseViewSet, basename="medibase") -# HCX -router.register("hcx/policy", PolicyViewSet, basename="hcx-policy") -router.register("hcx/claim", ClaimViewSet, basename="hcx-claim") -router.register("hcx/communication", CommunicationViewSet, basename="hcx-communication") -router.register("hcx", HcxGatewayViewSet) - # Public endpoints router.register("public/asset", AssetPublicViewSet, basename="public-asset") router.register("public/asset_qr", AssetPublicQRViewSet, basename="public-asset-qr") diff --git a/config/settings/base.py b/config/settings/base.py index 8cac4e189a..56322fbb4b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -122,7 +122,6 @@ "care.abdm", "care.users", "care.audit_log", - "care.hcx", ] PLUGIN_APPS = manager.get_apps() @@ -627,20 +626,6 @@ IS_PRODUCTION = False -# HCX -HCX_PROTOCOL_BASE_PATH = env( - "HCX_PROTOCOL_BASE_PATH", default="http://staging-hcx.swasth.app/api/v0.7" -) -HCX_AUTH_BASE_PATH = env( - "HCX_AUTH_BASE_PATH", - default="https://staging-hcx.swasth.app/auth/realms/swasth-health-claim-exchange/protocol/openid-connect/token", -) -HCX_PARTICIPANT_CODE = env("HCX_PARTICIPANT_CODE", default="") -HCX_USERNAME = env("HCX_USERNAME", default="") -HCX_PASSWORD = env("HCX_PASSWORD", default="") -HCX_ENCRYPTION_PRIVATE_KEY_URL = env("HCX_ENCRYPTION_PRIVATE_KEY_URL", default="") -HCX_IG_URL = env("HCX_IG_URL", default="https://ig.hcxprotocol.io/v0.7.1") - PLAUSIBLE_HOST = env("PLAUSIBLE_HOST", default="") PLAUSIBLE_SITE_ID = env("PLAUSIBLE_SITE_ID", default="") PLAUSIBLE_AUTH_TOKEN = env("PLAUSIBLE_AUTH_TOKEN", default="") diff --git a/config/urls.py b/config/urls.py index 4d112c686a..549450363e 100644 --- a/config/urls.py +++ b/config/urls.py @@ -14,12 +14,6 @@ from care.facility.api.viewsets.patient_consultation import ( dev_preview_discharge_summary, ) -from care.hcx.api.viewsets.listener import ( - ClaimOnSubmitView, - CommunicationRequestView, - CoverageElibilityOnCheckView, - PreAuthOnSubmitView, -) from care.users.api.viewsets.change_password import ChangePasswordView from care.users.reset_password_views import ( ResetPasswordCheck, @@ -72,27 +66,6 @@ name="change_password_view", ), path("api/v1/", include(api_router.urlpatterns)), - # Hcx Listeners - path( - "coverageeligibility/on_check", - CoverageElibilityOnCheckView.as_view(), - name="hcx_coverage_eligibility_on_check", - ), - path( - "preauth/on_submit", - PreAuthOnSubmitView.as_view(), - name="hcx_pre_auth_on_submit", - ), - path( - "claim/on_submit", - ClaimOnSubmitView.as_view(), - name="hcx_claim_on_submit", - ), - path( - "communication/request", - CommunicationRequestView.as_view(), - name="hcx_communication_on_request", - ), # Health check urls path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), path("middleware/verify-asset", MiddlewareAssetAuthenticationVerifyView.as_view()), diff --git a/plug_config.py b/plug_config.py index a99af83fc5..62328d9274 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,5 +1,38 @@ +import os + from plugs.manager import PlugManager +from plugs.plug import Plug + +HCX_PROTOCOL_BASE_PATH = os.getenv( + "HCX_PROTOCOL_BASE_PATH", default="http://staging-hcx.swasth.app/api/v0.7" +) +HCX_AUTH_BASE_PATH = os.getenv( + "HCX_AUTH_BASE_PATH", + default="https://staging-hcx.swasth.app/auth/realms/swasth-health-claim-exchange/protocol/openid-connect/token", +) +HCX_PARTICIPANT_CODE = os.getenv("HCX_PARTICIPANT_CODE", default="") +HCX_USERNAME = os.getenv("HCX_USERNAME", default="") +HCX_PASSWORD = os.getenv("HCX_PASSWORD", default="") +HCX_ENCRYPTION_PRIVATE_KEY_URL = os.getenv("HCX_ENCRYPTION_PRIVATE_KEY_URL", default="") +HCX_IG_URL = os.getenv("HCX_IG_URL", default="https://ig.hcxprotocol.io/v0.7.1") +AUTH_USER_MODEL = "users.User" + +hcx_plugin = Plug( + name="hcx", + package_name="/Users/khavinshankar/Documents/ohcnetwork/care_hcx", + version="@main", + configs={ + "HCX_PROTOCOL_BASE_PATH": HCX_PROTOCOL_BASE_PATH, + "HCX_AUTH_BASE_PATH": HCX_AUTH_BASE_PATH, + "HCX_PARTICIPANT_CODE": HCX_PARTICIPANT_CODE, + "HCX_USERNAME": HCX_USERNAME, + "HCX_PASSWORD": HCX_PASSWORD, + "HCX_ENCRYPTION_PRIVATE_KEY_URL": HCX_ENCRYPTION_PRIVATE_KEY_URL, + "HCX_IG_URL": HCX_IG_URL, + "AUTH_USER_MODEL": AUTH_USER_MODEL, + }, +) -plugs = [] +plugs = [hcx_plugin] manager = PlugManager(plugs) From 589e3cae3a04d20750a384fc065b8ca23b2fec33 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Sun, 25 Aug 2024 18:29:57 +0530 Subject: [PATCH 2/9] added HCX_CERT_URL env for hcx plug --- plug_config.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plug_config.py b/plug_config.py index 62328d9274..1d7d3688e6 100644 --- a/plug_config.py +++ b/plug_config.py @@ -13,8 +13,15 @@ HCX_PARTICIPANT_CODE = os.getenv("HCX_PARTICIPANT_CODE", default="") HCX_USERNAME = os.getenv("HCX_USERNAME", default="") HCX_PASSWORD = os.getenv("HCX_PASSWORD", default="") -HCX_ENCRYPTION_PRIVATE_KEY_URL = os.getenv("HCX_ENCRYPTION_PRIVATE_KEY_URL", default="") +HCX_ENCRYPTION_PRIVATE_KEY_URL = os.getenv( + "HCX_ENCRYPTION_PRIVATE_KEY_URL", + default="https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/hcx-apis/src/test/resources/examples/x509-private-key.pem", +) HCX_IG_URL = os.getenv("HCX_IG_URL", default="https://ig.hcxprotocol.io/v0.7.1") +HCX_CERT_URL = os.getenv( + "HCX_CERT_URL", + default="https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/hcx-apis/src/test/resources/examples/x509-self-signed-certificate.pem", +) AUTH_USER_MODEL = "users.User" hcx_plugin = Plug( @@ -29,6 +36,7 @@ "HCX_PASSWORD": HCX_PASSWORD, "HCX_ENCRYPTION_PRIVATE_KEY_URL": HCX_ENCRYPTION_PRIVATE_KEY_URL, "HCX_IG_URL": HCX_IG_URL, + "HCX_CERT_URL": HCX_CERT_URL, "AUTH_USER_MODEL": AUTH_USER_MODEL, }, ) From 9da2dd621a7009e9d6f44fedfcb34fe1b420f8c4 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Mon, 26 Aug 2024 11:12:03 +0530 Subject: [PATCH 3/9] updated hcx plug package name --- plug_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plug_config.py b/plug_config.py index 1d7d3688e6..a224384b4a 100644 --- a/plug_config.py +++ b/plug_config.py @@ -26,7 +26,7 @@ hcx_plugin = Plug( name="hcx", - package_name="/Users/khavinshankar/Documents/ohcnetwork/care_hcx", + package_name="git+https://github.com/coronasafe/care_hcx.git", version="@main", configs={ "HCX_PROTOCOL_BASE_PATH": HCX_PROTOCOL_BASE_PATH, From e8b189d80140ff6b8c9e3ec389a8d171e22a3d76 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Mon, 26 Aug 2024 11:18:12 +0530 Subject: [PATCH 4/9] updated hcx plug repo url --- plug_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plug_config.py b/plug_config.py index a224384b4a..ae9e895ae3 100644 --- a/plug_config.py +++ b/plug_config.py @@ -26,7 +26,7 @@ hcx_plugin = Plug( name="hcx", - package_name="git+https://github.com/coronasafe/care_hcx.git", + package_name="git+https://github.com/ohcnetwork/care_hcx.git", version="@main", configs={ "HCX_PROTOCOL_BASE_PATH": HCX_PROTOCOL_BASE_PATH, From 773e798fbd174b032312d7ce5b59fe8d7c82cbbd Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 27 Aug 2024 09:08:19 +0530 Subject: [PATCH 5/9] enabled loading static data from plugs --- .../management/commands/load_redis_index.py | 16 ++++++++++++++++ care/facility/tasks/redis_index.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/care/facility/management/commands/load_redis_index.py b/care/facility/management/commands/load_redis_index.py index 243ae95020..da31525d34 100644 --- a/care/facility/management/commands/load_redis_index.py +++ b/care/facility/management/commands/load_redis_index.py @@ -1,8 +1,11 @@ +from importlib import import_module + from django.core.cache import cache from django.core.management import BaseCommand from care.facility.static_data.icd11 import load_icd11_diagnosis from care.facility.static_data.medibase import load_medibase_medicines +from plug_config import manager class Command(BaseCommand): @@ -23,4 +26,17 @@ def handle(self, *args, **options): load_icd11_diagnosis() load_medibase_medicines() + for plug in manager.plugs: + try: + module_path = f"{plug.name}.static_data" + module = import_module(module_path) + + load_static_data = getattr(module, "load_static_data", None) + if load_static_data: + load_static_data() + except ModuleNotFoundError: + print(f"Module {module_path} not found") + except Exception as e: + print(f"Error loading static data for {plug.name}: {e}") + cache.delete("redis_index_loading") diff --git a/care/facility/tasks/redis_index.py b/care/facility/tasks/redis_index.py index e1281330eb..0dba4797e7 100644 --- a/care/facility/tasks/redis_index.py +++ b/care/facility/tasks/redis_index.py @@ -1,3 +1,4 @@ +from importlib import import_module from logging import Logger from celery import shared_task @@ -7,6 +8,7 @@ from care.facility.static_data.icd11 import load_icd11_diagnosis from care.facility.static_data.medibase import load_medibase_medicines from care.utils.static_data.models.base import index_exists +from plug_config import manager logger: Logger = get_task_logger(__name__) @@ -26,5 +28,18 @@ def load_redis_index(): load_icd11_diagnosis() load_medibase_medicines() + for plug in manager.plugs: + try: + module_path = f"{plug.name}.static_data" + module = import_module(module_path) + + load_static_data = getattr(module, "load_static_data", None) + if load_static_data: + load_static_data() + except ModuleNotFoundError: + logger.info(f"Module {module_path} not found") + except Exception as e: + logger.info(f"Error loading static data for {plug.name}: {e}") + cache.delete("redis_index_loading") logger.info("Redis Index Loaded") From e2b5e55009a8ef78c955ab3c49069681e5c195bc Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 17 Sep 2024 06:20:56 +0530 Subject: [PATCH 6/9] removed env declaration in plug config --- plug_config.py | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/plug_config.py b/plug_config.py index ae9e895ae3..c177c62cde 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,44 +1,11 @@ -import os - from plugs.manager import PlugManager from plugs.plug import Plug -HCX_PROTOCOL_BASE_PATH = os.getenv( - "HCX_PROTOCOL_BASE_PATH", default="http://staging-hcx.swasth.app/api/v0.7" -) -HCX_AUTH_BASE_PATH = os.getenv( - "HCX_AUTH_BASE_PATH", - default="https://staging-hcx.swasth.app/auth/realms/swasth-health-claim-exchange/protocol/openid-connect/token", -) -HCX_PARTICIPANT_CODE = os.getenv("HCX_PARTICIPANT_CODE", default="") -HCX_USERNAME = os.getenv("HCX_USERNAME", default="") -HCX_PASSWORD = os.getenv("HCX_PASSWORD", default="") -HCX_ENCRYPTION_PRIVATE_KEY_URL = os.getenv( - "HCX_ENCRYPTION_PRIVATE_KEY_URL", - default="https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/hcx-apis/src/test/resources/examples/x509-private-key.pem", -) -HCX_IG_URL = os.getenv("HCX_IG_URL", default="https://ig.hcxprotocol.io/v0.7.1") -HCX_CERT_URL = os.getenv( - "HCX_CERT_URL", - default="https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/hcx-apis/src/test/resources/examples/x509-self-signed-certificate.pem", -) -AUTH_USER_MODEL = "users.User" - hcx_plugin = Plug( name="hcx", package_name="git+https://github.com/ohcnetwork/care_hcx.git", version="@main", - configs={ - "HCX_PROTOCOL_BASE_PATH": HCX_PROTOCOL_BASE_PATH, - "HCX_AUTH_BASE_PATH": HCX_AUTH_BASE_PATH, - "HCX_PARTICIPANT_CODE": HCX_PARTICIPANT_CODE, - "HCX_USERNAME": HCX_USERNAME, - "HCX_PASSWORD": HCX_PASSWORD, - "HCX_ENCRYPTION_PRIVATE_KEY_URL": HCX_ENCRYPTION_PRIVATE_KEY_URL, - "HCX_IG_URL": HCX_IG_URL, - "HCX_CERT_URL": HCX_CERT_URL, - "AUTH_USER_MODEL": AUTH_USER_MODEL, - }, + configs={}, ) plugs = [hcx_plugin] From 8cdb876590d4374d1b656818cf437b5dfa5ef759 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 17 Sep 2024 06:26:35 +0530 Subject: [PATCH 7/9] fixed linting errors --- config/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings/base.py b/config/settings/base.py index eafb396577..0cf7459ece 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -6,10 +6,10 @@ import json from datetime import datetime, timedelta from pathlib import Path -from django.utils.translation import gettext_lazy as _ import environ from authlib.jose import JsonWebKey +from django.utils.translation import gettext_lazy as _ from healthy_django.healthcheck.celery_queue_length import ( DjangoCeleryQueueLengthHealthCheck, ) From cca51cf3f90e0a62d55e431044aa6e9e5fee8e51 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 17 Sep 2024 15:45:10 +0530 Subject: [PATCH 8/9] added hcx staging config for local and testing --- docker/.local.env | 10 ++++++++++ docker/.prebuilt.env | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/docker/.local.env b/docker/.local.env index ccfaef5fba..b00327fc9b 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -16,3 +16,13 @@ BUCKET_ENDPOINT=http://localstack:4566 BUCKET_EXTERNAL_ENDPOINT=http://localhost:4566 FILE_UPLOAD_BUCKET=patient-bucket FACILITY_S3_BUCKET=facility-bucket + +# HCX Sandbox Config for local and testing +HCX_AUTH_BASE_PATH=https://staging-hcx.swasth.app/auth/realms/swasth-health-claim-exchange/protocol/openid-connect/token +HCX_ENCRYPTION_PRIVATE_KEY_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-private-key.pem +HCX_IG_URL=https://ig.hcxprotocol.io/v0.7.1 +HCX_PARTICIPANT_CODE=qwertyreboot.gmail@swasth-hcx-staging +HCX_PASSWORD=Opensaber@123 +HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 +HCX_USERNAME=qwertyreboot@gmail.com +HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem diff --git a/docker/.prebuilt.env b/docker/.prebuilt.env index 02267439a7..8bcc36312e 100644 --- a/docker/.prebuilt.env +++ b/docker/.prebuilt.env @@ -29,3 +29,13 @@ DJANGO_SECURE_HSTS_PRELOAD=False DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=False DJANGO_SECURE_SSL_REDIRECT=False DJANGO_SECURE_CONTENT_TYPE_NOSNIFF=False + +# HCX Sandbox Config for local and testing +HCX_AUTH_BASE_PATH=https://staging-hcx.swasth.app/auth/realms/swasth-health-claim-exchange/protocol/openid-connect/token +HCX_ENCRYPTION_PRIVATE_KEY_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-private-key.pem +HCX_IG_URL=https://ig.hcxprotocol.io/v0.7.1 +HCX_PARTICIPANT_CODE=qwertyreboot.gmail@swasth-hcx-staging +HCX_PASSWORD=Opensaber@123 +HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 +HCX_USERNAME=qwertyreboot@gmail.com +HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem From a2f147fa58f08222255e69d4103bb1096da2b860 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 17 Sep 2024 16:33:12 +0530 Subject: [PATCH 9/9] fixed tests --- .../facility/tests/sample_reports/sample2.png | Bin 317643 -> 264282 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/care/facility/tests/sample_reports/sample2.png b/care/facility/tests/sample_reports/sample2.png index e46ee400018cd75312092000e6212fe70b8bfb00..468bf54b45bf7a9525626ad6cb6e359343f21b4d 100644 GIT binary patch delta 167 zcmX@TQTWybfeFe?9Ey#qt*VTzs!Ur|nLRGE|4)8~V)U&is`IP^htGJ2* O2s~Z=T-G@yGywqKaXMZA delta 53060 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