diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml
index e7761b0697..b376a6f98f 100644
--- a/.github/workflows/deployment-branch.yaml
+++ b/.github/workflows/deployment-branch.yaml
@@ -5,13 +5,13 @@ on:
push:
branches:
+ - abdm
- abdm-m2
- hcx-communications
paths-ignore:
- "docs/**"
jobs:
-
build-image:
name: Build & Push Staging to container registries
runs-on: ubuntu-latest
@@ -28,7 +28,6 @@ jobs:
type=raw,value=${{ github.ref_name}}-${{ github.run_number }}
type=raw,value=${{ github.ref_name}}
-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
diff --git a/aws/backend.json b/aws/backend.json
index ec3dc98474..a080073629 100644
--- a/aws/backend.json
+++ b/aws/backend.json
@@ -100,6 +100,26 @@
{
"name": "USE_S3",
"value": "True"
+ },
+ {
+ "name": "ENABLE_ABDM",
+ "value": "True"
+ },
+ {
+ "name": "ABDM_URL",
+ "value": "https://dev.abdm.gov.in"
+ },
+ {
+ "name": "HEALTH_SERVICE_API_URL",
+ "value": "https://healthidsbx.abdm.gov.in/api"
+ },
+ {
+ "name": "X_CM_ID",
+ "value": "sbx"
+ },
+ {
+ "name": "FIDELIUS_URL",
+ "value": "https://fidelius.ohc.network"
}
],
"repositoryCredentials": {
@@ -222,6 +242,14 @@
{
"valueFrom": "/care/backend/HCX_IG_URL",
"name": "HCX_IG_URL"
+ },
+ {
+ "valueFrom": "/care/backend/ABDM_CLIENT_ID",
+ "name": "ABDM_CLIENT_ID"
+ },
+ {
+ "valueFrom": "/care/backend/ABDM_CLIENT_SECRET",
+ "name": "ABDM_CLIENT_SECRET"
}
],
"name": "care-backend"
diff --git a/aws/celery.json b/aws/celery.json
index e79e4f66be..52cbb8b8f7 100644
--- a/aws/celery.json
+++ b/aws/celery.json
@@ -96,8 +96,8 @@
"repositoryCredentials": {
"credentialsParameter": "arn:aws:secretsmanager:ap-south-1:299650323339:secret:/github/pat-UHmr0B"
},
- "memory": 256,
- "memoryReservation": 256,
+ "memory": 512,
+ "memoryReservation": 512,
"workingDirectory": "/app",
"secrets": [
{
@@ -232,8 +232,8 @@
"/app/celery_worker-ecs.sh"
],
"cpu": 384,
- "memory": 768,
- "memoryReservation": 768,
+ "memory": 1536,
+ "memoryReservation": 1536,
"environment": [
{
"name": "AUDIT_LOG_ENABLED",
@@ -437,7 +437,7 @@
"name": "care-celery-worker"
}
],
- "memory": "1024",
+ "memory": "2048",
"taskRoleArn": "arn:aws:iam::299650323339:role/ecsTaskExecutionRole",
"family": "care-celery",
"requiresCompatibilities": [
diff --git a/care/abdm/__init__.py b/care/abdm/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/care/abdm/admin.py b/care/abdm/admin.py
new file mode 100644
index 0000000000..846f6b4061
--- /dev/null
+++ b/care/abdm/admin.py
@@ -0,0 +1 @@
+# Register your models here.
diff --git a/care/abdm/api/__init__.py b/care/abdm/api/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/care/abdm/api/serializers/abha.py b/care/abdm/api/serializers/abha.py
new file mode 100644
index 0000000000..c4d88dbc0f
--- /dev/null
+++ b/care/abdm/api/serializers/abha.py
@@ -0,0 +1,9 @@
+from rest_framework.serializers import ModelSerializer
+
+from care.abdm.models import AbhaNumber
+
+
+class AbhaSerializer(ModelSerializer):
+ class Meta:
+ exclude = ("deleted",)
+ model = AbhaNumber
diff --git a/care/abdm/api/serializers/abhanumber.py b/care/abdm/api/serializers/abhanumber.py
new file mode 100644
index 0000000000..1dfdf95d4e
--- /dev/null
+++ b/care/abdm/api/serializers/abhanumber.py
@@ -0,0 +1,10 @@
+# ModelSerializer
+from rest_framework import serializers
+
+from care.abdm.models import AbhaNumber
+
+
+class AbhaNumberSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = AbhaNumber
+ exclude = ("access_token", "refresh_token", "txn_id")
diff --git a/care/abdm/api/serializers/auth.py b/care/abdm/api/serializers/auth.py
new file mode 100644
index 0000000000..9b533d0c9b
--- /dev/null
+++ b/care/abdm/api/serializers/auth.py
@@ -0,0 +1,24 @@
+from rest_framework.serializers import CharField, IntegerField, Serializer
+
+
+class AbdmAuthResponseSerializer(Serializer):
+ """
+ Serializer for the response of the authentication API
+ """
+
+ accessToken = CharField()
+ refreshToken = CharField()
+ expiresIn = IntegerField()
+ refreshExpiresIn = IntegerField()
+ tokenType = CharField()
+
+
+class AbdmAuthInitResponseSerializer(Serializer):
+ """
+ Serializer for the response of the authentication API
+ """
+
+ token = CharField()
+ refreshToken = CharField()
+ expiresIn = IntegerField()
+ refreshExpiresIn = IntegerField()
diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py
new file mode 100644
index 0000000000..aa2b7cc1fd
--- /dev/null
+++ b/care/abdm/api/serializers/healthid.py
@@ -0,0 +1,61 @@
+from rest_framework.serializers import CharField, Serializer, UUIDField
+
+
+class AadharOtpGenerateRequestPayloadSerializer(Serializer):
+ aadhaar = CharField(max_length=16, min_length=12, required=True)
+
+
+class AadharOtpResendRequestPayloadSerializer(Serializer):
+ txnId = CharField(max_length=64, min_length=1, required=True)
+
+
+class HealthIdSerializer(Serializer):
+ healthId = CharField(max_length=64, min_length=1, required=True)
+
+
+class QRContentSerializer(Serializer):
+ hidn = CharField(max_length=17, min_length=17, required=True)
+ phr = CharField(max_length=64, min_length=1, required=True)
+ name = CharField(max_length=64, min_length=1, required=True)
+ gender = CharField(max_length=1, min_length=1, required=True)
+ dob = CharField(max_length=10, min_length=8, required=True)
+
+
+class HealthIdAuthSerializer(Serializer):
+ authMethod = CharField(max_length=64, min_length=1, required=True)
+ healthid = CharField(max_length=64, min_length=1, required=True)
+
+
+class ABHASearchRequestSerializer:
+ name = CharField(max_length=64, min_length=1, required=False)
+ mobile = CharField(
+ max_length=10,
+ min_length=10,
+ required=False,
+ )
+ gender = CharField(max_length=1, min_length=1, required=False)
+ yearOfBirth = CharField(max_length=4, min_length=4, required=False)
+
+
+class GenerateMobileOtpRequestPayloadSerializer(Serializer):
+ mobile = CharField(max_length=10, min_length=10, required=True)
+ txnId = CharField(max_length=64, min_length=1, required=True)
+
+
+class VerifyOtpRequestPayloadSerializer(Serializer):
+ otp = CharField(max_length=6, min_length=6, required=True, help_text="OTP")
+ txnId = CharField(max_length=64, min_length=1, required=True)
+ patientId = UUIDField(required=False)
+
+
+class VerifyDemographicsRequestPayloadSerializer(Serializer):
+ gender = CharField(max_length=10, min_length=1, required=True)
+ name = CharField(max_length=64, min_length=1, required=True)
+ yearOfBirth = CharField(max_length=4, min_length=4, required=True)
+ txnId = CharField(max_length=64, min_length=1, required=True)
+
+
+class CreateHealthIdSerializer(Serializer):
+ healthId = CharField(max_length=64, min_length=1, required=False)
+ txnId = CharField(max_length=64, min_length=1, required=True)
+ patientId = UUIDField(required=False)
diff --git a/care/abdm/api/serializers/hip.py b/care/abdm/api/serializers/hip.py
new file mode 100644
index 0000000000..4e3bb0f9ab
--- /dev/null
+++ b/care/abdm/api/serializers/hip.py
@@ -0,0 +1,33 @@
+from rest_framework.serializers import CharField, IntegerField, Serializer
+
+
+class AddressSerializer(Serializer):
+ line = CharField()
+ district = CharField()
+ state = CharField()
+ pincode = CharField()
+
+
+class PatientSerializer(Serializer):
+ healthId = CharField(allow_null=True)
+ healthIdNumber = CharField()
+ name = CharField()
+ gender = CharField()
+ yearOfBirth = IntegerField()
+ dayOfBirth = IntegerField()
+ monthOfBirth = IntegerField()
+ address = AddressSerializer()
+
+
+class ProfileSerializer(Serializer):
+ hipCode = CharField()
+ patient = PatientSerializer()
+
+
+class HipShareProfileSerializer(Serializer):
+ """
+ Serializer for the request of the share_profile
+ """
+
+ requestId = CharField()
+ profile = ProfileSerializer()
diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py
new file mode 100644
index 0000000000..7fcf102850
--- /dev/null
+++ b/care/abdm/api/viewsets/abha.py
@@ -0,0 +1,38 @@
+from rest_framework.decorators import action
+from rest_framework.generics import get_object_or_404
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.viewsets import GenericViewSet
+
+from care.abdm.api.serializers.abha import AbhaSerializer
+from care.abdm.models import AbhaNumber
+from care.abdm.utils.api_call import HealthIdGateway
+from care.utils.queryset.patient import get_patient_queryset
+
+
+class AbhaViewSet(GenericViewSet):
+ serializer_class = AbhaSerializer
+ model = AbhaNumber
+ queryset = AbhaNumber.objects.all()
+ permission_classes = (IsAuthenticated,)
+
+ def get_abha_object(self):
+ queryset = get_patient_queryset(self.request.user)
+ patient_obj = get_object_or_404(
+ queryset.filter(external_id=self.kwargs.get("patient_external_id"))
+ )
+ return patient_obj.abha_number
+
+ @action(detail=False, methods=["GET"])
+ def get_qr_code(self, request, *args, **kwargs):
+ obj = self.get_abha_object()
+ gateway = HealthIdGateway()
+ response = gateway.get_qr_code(obj)
+ return Response(response)
+
+ @action(detail=False, methods=["GET"])
+ def get_profile(self, request, *args, **kwargs):
+ obj = self.get_abha_object()
+ gateway = HealthIdGateway()
+ response = gateway.get_profile(obj)
+ return Response(response)
diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py
new file mode 100644
index 0000000000..e72b3975d7
--- /dev/null
+++ b/care/abdm/api/viewsets/auth.py
@@ -0,0 +1,342 @@
+import json
+from datetime import datetime, timedelta
+
+from django.core.cache import cache
+from rest_framework import status
+from rest_framework.generics import GenericAPIView, get_object_or_404
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+
+from care.abdm.utils.api_call import AbdmGateway
+from care.abdm.utils.cipher import Cipher
+from care.abdm.utils.fhir import Fhir
+from care.facility.models.patient import PatientRegistration
+from care.facility.models.patient_consultation import PatientConsultation
+from config.authentication import ABDMAuthentication
+
+
+class OnFetchView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ AbdmGateway().init(data["resp"]["requestId"])
+
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+
+
+class OnInitView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"])
+
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+
+
+class OnConfirmView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ if "validity" in data["auth"]:
+ if data["auth"]["validity"]["purpose"] == "LINK":
+ AbdmGateway().add_care_context(
+ data["auth"]["accessToken"],
+ data["resp"]["requestId"],
+ )
+ else:
+ AbdmGateway().save_linking_token(
+ data["auth"]["patient"],
+ data["auth"]["accessToken"],
+ data["resp"]["requestId"],
+ )
+ else:
+ AbdmGateway().save_linking_token(
+ data["auth"]["patient"],
+ data["auth"]["accessToken"],
+ data["resp"]["requestId"],
+ )
+ AbdmGateway().add_care_context(
+ data["auth"]["accessToken"],
+ data["resp"]["requestId"],
+ )
+
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+
+
+class AuthNotifyView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ if data["auth"]["status"] != "GRANTED":
+ return
+
+ AbdmGateway.auth_on_notify({"request_id": data["auth"]["transactionId"]})
+
+ # AbdmGateway().add_care_context(
+ # data["auth"]["accessToken"],
+ # data["resp"]["requestId"],
+ # )
+
+
+class OnAddContextsView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+
+
+class DiscoverView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ patients = PatientRegistration.objects.all()
+ verified_identifiers = data["patient"]["verifiedIdentifiers"]
+ matched_by = []
+ if len(verified_identifiers) == 0:
+ return Response(
+ "No matching records found, need more data",
+ status=status.HTTP_404_NOT_FOUND,
+ )
+ else:
+ for identifier in verified_identifiers:
+ if identifier["value"] is None:
+ continue
+
+ # if identifier["type"] == "MOBILE":
+ # matched_by.append(identifier["value"])
+ # mobile = identifier["value"].replace("+91", "").replace("-", "")
+ # patients = patients.filter(
+ # Q(phone_number=f"+91{mobile}") | Q(phone_number=mobile)
+ # )
+
+ if identifier["type"] == "NDHM_HEALTH_NUMBER":
+ matched_by.append(identifier["value"])
+ patients = patients.filter(
+ abha_number__abha_number=identifier["value"]
+ )
+
+ if identifier["type"] == "HEALTH_ID":
+ matched_by.append(identifier["value"])
+ patients = patients.filter(
+ abha_number__health_id=identifier["value"]
+ )
+
+ # TODO: also filter by demographics
+ patient = patients.last()
+
+ if not patient:
+ return Response(
+ "No matching records found, need more data",
+ status=status.HTTP_404_NOT_FOUND,
+ )
+
+ AbdmGateway().on_discover(
+ {
+ "request_id": data["requestId"],
+ "transaction_id": data["transactionId"],
+ "patient_id": str(patient.external_id),
+ "patient_name": patient.name,
+ "care_contexts": list(
+ map(
+ lambda consultation: {
+ "id": str(consultation.external_id),
+ "name": f"Encounter: {str(consultation.created_date.date())}",
+ },
+ PatientConsultation.objects.filter(patient=patient),
+ )
+ ),
+ "matched_by": matched_by,
+ }
+ )
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+
+
+class LinkInitView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ # TODO: send otp to patient
+
+ AbdmGateway().on_link_init(
+ {
+ "request_id": data["requestId"],
+ "transaction_id": data["transactionId"],
+ "patient_id": data["patient"]["referenceNumber"],
+ "phone": "7639899448",
+ }
+ )
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+
+
+class LinkConfirmView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ # TODO: verify otp
+
+ patient = get_object_or_404(
+ PatientRegistration.objects.filter(
+ external_id=data["confirmation"]["linkRefNumber"]
+ )
+ )
+ AbdmGateway().on_link_confirm(
+ {
+ "request_id": data["requestId"],
+ "patient_id": str(patient.external_id),
+ "patient_name": patient.name,
+ "care_contexts": list(
+ map(
+ lambda consultation: {
+ "id": str(consultation.external_id),
+ "name": f"Encounter: {str(consultation.created_date.date())}",
+ },
+ PatientConsultation.objects.filter(patient=patient),
+ )
+ ),
+ }
+ )
+
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+
+
+class NotifyView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ cache.set(data["notification"]["consentId"], json.dumps(data))
+
+ AbdmGateway().on_notify(
+ {
+ "request_id": data["requestId"],
+ "consent_id": data["notification"]["consentId"],
+ }
+ )
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+
+
+class RequestDataView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ consent_id = data["hiRequest"]["consent"]["id"]
+ consent = json.loads(cache.get(consent_id)) if consent_id in cache else None
+ if not consent or not consent["notification"]["status"] == "GRANTED":
+ return Response({}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # TODO: check if from and to are in range and consent expiry is greater than today
+ # consent_from = datetime.fromisoformat(
+ # consent["notification"]["permission"]["dateRange"]["from"][:-1]
+ # )
+ # consent_to = datetime.fromisoformat(
+ # consent["notification"]["permission"]["dateRange"]["to"][:-1]
+ # )
+ # now = datetime.now()
+ # if not consent_from < now and now > consent_to:
+ # return Response({}, status=status.HTTP_403_FORBIDDEN)
+
+ on_data_request_response = AbdmGateway().on_data_request(
+ {"request_id": data["requestId"], "transaction_id": data["transactionId"]}
+ )
+
+ if not on_data_request_response.status_code == 202:
+ return Response({}, status=status.HTTP_202_ACCEPTED)
+ return Response(
+ on_data_request_response, status=status.HTTP_400_BAD_REQUEST
+ )
+
+ cipher = Cipher(
+ data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"],
+ data["hiRequest"]["keyMaterial"]["nonce"],
+ )
+
+ AbdmGateway().data_transfer(
+ {
+ "transaction_id": data["transactionId"],
+ "data_push_url": data["hiRequest"]["dataPushUrl"],
+ "care_contexts": sum(
+ list(
+ map(
+ lambda context: list(
+ map(
+ lambda record: {
+ "patient_id": context["patientReference"],
+ "consultation_id": context[
+ "careContextReference"
+ ],
+ "data": cipher.encrypt(
+ Fhir(
+ PatientConsultation.objects.get(
+ external_id=context[
+ "careContextReference"
+ ]
+ )
+ ).create_record(record)
+ )["data"],
+ },
+ consent["notification"]["consentDetail"]["hiTypes"],
+ )
+ ),
+ consent["notification"]["consentDetail"]["careContexts"][
+ :-2:-1
+ ],
+ )
+ ),
+ [],
+ ),
+ "key_material": {
+ "cryptoAlg": "ECDH",
+ "curve": "Curve25519",
+ "dhPublicKey": {
+ "expiry": (datetime.now() + timedelta(days=2)).isoformat(),
+ "parameters": "Curve25519/32byte random key",
+ "keyValue": cipher.key_to_share,
+ },
+ "nonce": cipher.sender_nonce,
+ },
+ }
+ )
+
+ AbdmGateway().data_notify(
+ {
+ "consent_id": data["hiRequest"]["consent"]["id"],
+ "transaction_id": data["transactionId"],
+ "care_contexts": list(
+ map(
+ lambda context: {"id": context["careContextReference"]},
+ consent["notification"]["consentDetail"]["careContexts"][
+ :-2:-1
+ ],
+ )
+ ),
+ }
+ )
+
+ return Response({}, status=status.HTTP_202_ACCEPTED)
diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py
new file mode 100644
index 0000000000..c30818eac0
--- /dev/null
+++ b/care/abdm/api/viewsets/healthid.py
@@ -0,0 +1,648 @@
+# ABDM HealthID APIs
+
+from datetime import datetime
+
+from drf_spectacular.utils import extend_schema
+from rest_framework import status
+from rest_framework.decorators import action
+from rest_framework.exceptions import ValidationError
+from rest_framework.mixins import CreateModelMixin
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.viewsets import GenericViewSet
+
+from care.abdm.api.serializers.abhanumber import AbhaNumberSerializer
+from care.abdm.api.serializers.healthid import (
+ AadharOtpGenerateRequestPayloadSerializer,
+ AadharOtpResendRequestPayloadSerializer,
+ CreateHealthIdSerializer,
+ GenerateMobileOtpRequestPayloadSerializer,
+ HealthIdAuthSerializer,
+ HealthIdSerializer,
+ QRContentSerializer,
+ VerifyDemographicsRequestPayloadSerializer,
+ VerifyOtpRequestPayloadSerializer,
+)
+from care.abdm.models import AbhaNumber
+from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway
+from care.facility.api.serializers.patient import PatientDetailSerializer
+from care.facility.models.patient import PatientConsultation, PatientRegistration
+from care.utils.queryset.patient import get_patient_queryset
+from config.auth_views import CaptchaRequiredException
+from config.ratelimit import ratelimit
+
+
+# API for Generating OTP for HealthID
+class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin):
+ base_name = "healthid"
+ model = AbhaNumber
+ permission_classes = (IsAuthenticated,)
+
+ @extend_schema(
+ operation_id="generate_aadhaar_otp",
+ request=AadharOtpGenerateRequestPayloadSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def generate_aadhaar_otp(self, request):
+ data = request.data
+
+ if ratelimit(request, "generate_aadhaar_otp", [data["aadhaar"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = AadharOtpGenerateRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().generate_aadhaar_otp(data)
+ return Response(response, status=status.HTTP_200_OK)
+
+ @extend_schema(
+ # /v1/registration/aadhaar/resendAadhaarOtp
+ operation_id="resend_aadhaar_otp",
+ request=AadharOtpResendRequestPayloadSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def resend_aadhaar_otp(self, request):
+ data = request.data
+
+ if ratelimit(request, "resend_aadhaar_otp", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = AadharOtpResendRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().resend_aadhaar_otp(data)
+ return Response(response, status=status.HTTP_200_OK)
+
+ @extend_schema(
+ # /v1/registration/aadhaar/verifyAadhaarOtp
+ operation_id="verify_aadhaar_otp",
+ request=VerifyOtpRequestPayloadSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def verify_aadhaar_otp(self, request):
+ data = request.data
+
+ if ratelimit(request, "verify_aadhaar_otp", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = VerifyOtpRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().verify_aadhaar_otp(
+ data
+ ) # HealthIdGatewayV2().verify_document_mobile_otp(data)
+ return Response(response, status=status.HTTP_200_OK)
+
+ @extend_schema(
+ # /v1/registration/aadhaar/generateMobileOTP
+ operation_id="generate_mobile_otp",
+ request=GenerateMobileOtpRequestPayloadSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def generate_mobile_otp(self, request):
+ data = request.data
+
+ if ratelimit(request, "generate_mobile_otp", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = GenerateMobileOtpRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().generate_mobile_otp(data)
+ return Response(response, status=status.HTTP_200_OK)
+
+ @extend_schema(
+ # /v1/registration/aadhaar/verifyMobileOTP
+ operation_id="verify_mobile_otp",
+ request=VerifyOtpRequestPayloadSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def verify_mobile_otp(self, request):
+ data = request.data
+
+ if ratelimit(request, "verify_mobile_otp", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = VerifyOtpRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().verify_mobile_otp(data)
+ return Response(response, status=status.HTTP_200_OK)
+
+ def create_abha(self, abha_profile, token):
+ abha_object = AbhaNumber.objects.filter(
+ abha_number=abha_profile["healthIdNumber"]
+ ).first()
+
+ if abha_object:
+ return abha_object
+
+ abha_object = AbhaNumber.objects.create(
+ abha_number=abha_profile["healthIdNumber"],
+ health_id=abha_profile["healthId"],
+ name=abha_profile["name"],
+ first_name=abha_profile["firstName"],
+ middle_name=abha_profile["middleName"],
+ last_name=abha_profile["lastName"],
+ gender=abha_profile["gender"],
+ date_of_birth=str(
+ datetime.strptime(
+ f"{abha_profile['yearOfBirth']}-{abha_profile['monthOfBirth']}-{abha_profile['dayOfBirth']}",
+ "%Y-%m-%d",
+ )
+ )[0:10],
+ address=abha_profile["address"] if "address" in abha_profile else "",
+ district=abha_profile["districtName"],
+ state=abha_profile["stateName"],
+ pincode=abha_profile["pincode"],
+ email=abha_profile["email"],
+ profile_photo=abha_profile["profilePhoto"],
+ txn_id=token["txn_id"],
+ access_token=token["access_token"],
+ refresh_token=token["refresh_token"],
+ )
+ abha_object.save()
+
+ return abha_object
+
+ def add_abha_details_to_patient(self, abha_object, patient_object):
+ patient_object.abha_number = abha_object
+ patient_object.save()
+ return True
+
+ @extend_schema(
+ # /v1/registration/aadhaar/createHealthId
+ operation_id="create_health_id",
+ request=CreateHealthIdSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def create_health_id(self, request):
+ data = request.data
+
+ if ratelimit(request, "create_health_id", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = CreateHealthIdSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ abha_profile = HealthIdGateway().create_health_id(data)
+
+ if "token" not in abha_profile:
+ return Response(
+ abha_profile,
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ # have a serializer to verify data of abha_profile
+ abha_object = self.create_abha(
+ abha_profile,
+ {
+ "txn_id": data["txnId"],
+ "access_token": abha_profile["token"],
+ "refresh_token": abha_profile["refreshToken"],
+ },
+ )
+
+ if "patientId" in data:
+ patient_id = data.pop("patientId")
+ allowed_patients = get_patient_queryset(request.user)
+ patient_obj = allowed_patients.filter(external_id=patient_id).first()
+ if not patient_obj:
+ raise ValidationError({"patient": "Not Found"})
+
+ if not self.add_abha_details_to_patient(
+ abha_object,
+ patient_obj,
+ ):
+ return Response(
+ {"message": "Failed to add abha Number to the patient"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ return Response(
+ {"id": abha_object.external_id, "abha_profile": abha_profile},
+ status=status.HTTP_200_OK,
+ )
+
+ # APIs to Find & Link Existing HealthID
+ # searchByHealthId
+ @extend_schema(
+ # /v1/registration/aadhaar/searchByHealthId
+ operation_id="search_by_health_id",
+ request=HealthIdSerializer,
+ responses={"200": "{'status': 'boolean'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def search_by_health_id(self, request):
+ data = request.data
+
+ if ratelimit(
+ request, "search_by_health_id", [data["healthId"]], increment=False
+ ):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = HealthIdSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().search_by_health_id(data)
+ return Response(response, status=status.HTTP_200_OK)
+
+ @action(detail=False, methods=["post"])
+ def get_abha_card(self, request):
+ data = request.data
+
+ if ratelimit(request, "get_abha_card", [data["patient"]], increment=False):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ allowed_patients = get_patient_queryset(request.user)
+ patient = allowed_patients.filter(external_id=data["patient"]).first()
+ if not patient:
+ raise ValidationError({"patient": "Not Found"})
+
+ if not patient.abha_number:
+ raise ValidationError({"abha": "Patient hasn't linked thier abha"})
+
+ if data["type"] == "png":
+ response = HealthIdGateway().get_abha_card_png(
+ {"refreshToken": patient.abha_number.refresh_token}
+ )
+ return Response(response, status=status.HTTP_200_OK)
+
+ response = HealthIdGateway().get_abha_card_pdf(
+ {"refreshToken": patient.abha_number.refresh_token}
+ )
+ return Response(response, status=status.HTTP_200_OK)
+
+ @extend_schema(
+ # /v1/registration/aadhaar/searchByHealthId
+ operation_id="link_via_qr",
+ request=HealthIdSerializer,
+ responses={"200": "{'status': 'boolean'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def link_via_qr(self, request):
+ data = request.data
+
+ if ratelimit(request, "link_via_qr", [data["hidn"]], increment=False):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = QRContentSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+
+ dob = datetime.strptime(data["dob"], "%d-%m-%Y").date()
+
+ patient = PatientRegistration.objects.filter(
+ abha_number__abha_number=data["hidn"]
+ ).first()
+ if patient:
+ return Response(
+ {
+ "message": "A patient is already associated with the provided Abha Number"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ abha_number = AbhaNumber.objects.filter(abha_number=data["hidn"]).first()
+
+ if not abha_number:
+ abha_number = AbhaNumber.objects.create(
+ abha_number=data["hidn"],
+ health_id=data["phr"],
+ name=data["name"],
+ gender=data["gender"],
+ date_of_birth=str(dob)[0:10],
+ address=data["address"],
+ district=data["dist name"],
+ state=data["state name"],
+ )
+
+ abha_number.save()
+
+ AbdmGateway().fetch_modes(
+ {
+ "healthId": data["phr"] or data["hidn"],
+ "name": data["name"],
+ "gender": data["gender"],
+ "dateOfBirth": str(datetime.strptime(data["dob"], "%d-%m-%Y"))[
+ 0:10
+ ],
+ }
+ )
+
+ if "patientId" in data and data["patientId"] is not None:
+ patient = PatientRegistration.objects.filter(
+ external_id=data["patientId"]
+ ).first()
+
+ if not patient:
+ return Response(
+ {"message": "Enter a valid patientId"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ patient.abha_number = abha_number
+ patient.save()
+
+ abha_serialized = AbhaNumberSerializer(abha_number).data
+ return Response(
+ {"id": abha_serialized["external_id"], "abha_profile": abha_serialized},
+ status=status.HTTP_200_OK,
+ )
+
+ @extend_schema(
+ operation_id="get_new_linking_token",
+ responses={"200": "{'status': 'boolean'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def get_new_linking_token(self, request):
+ data = request.data
+
+ if ratelimit(request, "get_new_linking_token", [data["patient"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ patient = PatientDetailSerializer(
+ PatientRegistration.objects.get(external_id=data["patient"])
+ ).data
+
+ AbdmGateway().fetch_modes(
+ {
+ "healthId": patient["abha_number_object"]["abha_number"],
+ "name": patient["abha_number_object"]["name"],
+ "gender": patient["abha_number_object"]["gender"],
+ "dateOfBirth": str(patient["abha_number_object"]["date_of_birth"]),
+ }
+ )
+
+ return Response({}, status=status.HTTP_200_OK)
+
+ @action(detail=False, methods=["POST"])
+ def add_care_context(self, request, *args, **kwargs):
+ consultation_id = request.data["consultation"]
+
+ if ratelimit(request, "add_care_context", [consultation_id]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ consultation = PatientConsultation.objects.get(external_id=consultation_id)
+
+ if not consultation:
+ return Response(
+ {"consultation": "No matching records found"},
+ status=status.HTTP_404_NOT_FOUND,
+ )
+
+ AbdmGateway().fetch_modes(
+ {
+ "healthId": consultation.patient.abha_number.health_id,
+ "name": request.data["name"]
+ if "name" in request.data
+ else consultation.patient.abha_number.name,
+ "gender": request.data["gender"]
+ if "gender" in request.data
+ else consultation.patient.abha_number.gender,
+ "dateOfBirth": request.data["dob"]
+ if "dob" in request.data
+ else str(consultation.patient.abha_number.date_of_birth),
+ "consultationId": consultation_id,
+ # "authMode": "DIRECT",
+ "purpose": "LINK",
+ }
+ )
+
+ return Response(status=status.HTTP_202_ACCEPTED)
+
+ @action(detail=False, methods=["POST"])
+ def patient_sms_notify(self, request, *args, **kwargs):
+ patient_id = request.data["patient"]
+
+ if ratelimit(request, "patient_sms_notify", [patient_id]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ patient = PatientRegistration.objects.filter(external_id=patient_id).first()
+
+ if not patient:
+ return Response(
+ {"consultation": "No matching records found"},
+ status=status.HTTP_404_NOT_FOUND,
+ )
+
+ response = AbdmGateway().patient_sms_notify({"phone": patient.phone_number})
+
+ return Response(response, status=status.HTTP_202_ACCEPTED)
+
+ # auth/init
+ @extend_schema(
+ # /v1/auth/init
+ operation_id="auth_init",
+ request=HealthIdAuthSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def auth_init(self, request):
+ data = request.data
+
+ if ratelimit(request, "auth_init", [data["healthid"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = HealthIdAuthSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().auth_init(data)
+ return Response(response, status=status.HTTP_200_OK)
+
+ # /v1/auth/confirmWithAadhaarOtp
+ @extend_schema(
+ operation_id="confirm_with_aadhaar_otp",
+ request=VerifyOtpRequestPayloadSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def confirm_with_aadhaar_otp(self, request):
+ data = request.data
+
+ if ratelimit(request, "confirm_with_aadhaar_otp", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = VerifyOtpRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().confirm_with_aadhaar_otp(data)
+ abha_profile = HealthIdGateway().get_profile(response)
+
+ # have a serializer to verify data of abha_profile
+ abha_object = self.create_abha(
+ abha_profile,
+ {
+ "access_token": response["token"],
+ "refresh_token": response["refreshToken"],
+ "txn_id": data["txnId"],
+ },
+ )
+
+ if "patientId" in data:
+ patient_id = data.pop("patientId")
+ allowed_patients = get_patient_queryset(request.user)
+ patient_obj = allowed_patients.filter(external_id=patient_id).first()
+ if not patient_obj:
+ raise ValidationError({"patient": "Not Found"})
+
+ if not self.add_abha_details_to_patient(
+ abha_object,
+ patient_obj,
+ ):
+ return Response(
+ {"message": "Failed to add abha Number to the patient"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ return Response(
+ {"id": abha_object.external_id, "abha_profile": abha_profile},
+ status=status.HTTP_200_OK,
+ )
+
+ # /v1/auth/confirmWithMobileOtp
+ @extend_schema(
+ operation_id="confirm_with_mobile_otp",
+ request=VerifyOtpRequestPayloadSerializer,
+ # responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def confirm_with_mobile_otp(self, request):
+ data = request.data
+
+ if ratelimit(request, "confirm_with_mobile_otp", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = VerifyOtpRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().confirm_with_mobile_otp(data)
+ abha_profile = HealthIdGateway().get_profile(response)
+
+ # have a serializer to verify data of abha_profile
+ abha_object = self.create_abha(
+ abha_profile,
+ {
+ "access_token": response["token"],
+ "refresh_token": response["refreshToken"],
+ "txn_id": data["txnId"],
+ },
+ )
+
+ if "patientId" in data:
+ patient_id = data.pop("patientId")
+ allowed_patients = get_patient_queryset(request.user)
+ patient_obj = allowed_patients.filter(external_id=patient_id).first()
+ if not patient_obj:
+ raise ValidationError({"patient": "Not Found"})
+
+ if not self.add_abha_details_to_patient(
+ abha_object,
+ patient_obj,
+ ):
+ return Response(
+ {"message": "Failed to add abha Number to the patient"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ return Response(
+ {"id": abha_object.external_id, "abha_profile": abha_profile},
+ status=status.HTTP_200_OK,
+ )
+
+ @extend_schema(
+ operation_id="confirm_with_demographics",
+ request=VerifyDemographicsRequestPayloadSerializer,
+ responses={"200": "{'status': true}"},
+ tags=["ABDM HealthID"],
+ )
+ @action(detail=False, methods=["post"])
+ def confirm_with_demographics(self, request):
+ data = request.data
+
+ if ratelimit(request, "confirm_with_demographics", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = VerifyDemographicsRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().confirm_with_demographics(data)
+ return Response(response, status=status.HTTP_200_OK)
+
+ ############################################################################################################
+ # HealthID V2 APIs
+ @extend_schema(
+ # /v2/registration/aadhaar/checkAndGenerateMobileOTP
+ operation_id="check_and_generate_mobile_otp",
+ request=GenerateMobileOtpRequestPayloadSerializer,
+ responses={"200": "{'txnId': 'string'}"},
+ tags=["ABDM HealthID V2"],
+ )
+ @action(detail=False, methods=["post"])
+ def check_and_generate_mobile_otp(self, request):
+ data = request.data
+
+ if ratelimit(request, "check_and_generate_mobile_otp", [data["txnId"]]):
+ raise CaptchaRequiredException(
+ detail={"status": 429, "detail": "Too Many Requests Provide Captcha"},
+ code=status.HTTP_429_TOO_MANY_REQUESTS,
+ )
+
+ serializer = GenerateMobileOtpRequestPayloadSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+ response = HealthIdGateway().check_and_generate_mobile_otp(data)
+ return Response(response, status=status.HTTP_200_OK)
diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py
new file mode 100644
index 0000000000..ac93ef17de
--- /dev/null
+++ b/care/abdm/api/viewsets/hip.py
@@ -0,0 +1,139 @@
+import uuid
+from datetime import datetime, timezone
+
+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.abdm.api.serializers.hip import HipShareProfileSerializer
+from care.abdm.models import AbhaNumber
+from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway
+from care.facility.models.facility import Facility
+from care.facility.models.patient import PatientRegistration
+from config.authentication import ABDMAuthentication
+
+
+class HipViewSet(GenericViewSet):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def get_linking_token(self, data):
+ AbdmGateway().fetch_modes(data)
+ return True
+
+ @action(detail=False, methods=["POST"])
+ def share(self, request, *args, **kwargs):
+ data = request.data
+
+ patient_data = data["profile"]["patient"]
+ counter_id = (
+ data["profile"]["hipCode"]
+ if len(data["profile"]["hipCode"]) == 36
+ else Facility.objects.first().external_id
+ )
+
+ patient_data["mobile"] = ""
+ for identifier in patient_data["identifiers"]:
+ if identifier["type"] == "MOBILE":
+ patient_data["mobile"] = identifier["value"]
+
+ serializer = HipShareProfileSerializer(data=data)
+ serializer.is_valid(raise_exception=True)
+
+ if HealthIdGateway().verify_demographics(
+ patient_data["healthIdNumber"],
+ patient_data["name"],
+ patient_data["gender"],
+ patient_data["yearOfBirth"],
+ ):
+ patient = PatientRegistration.objects.filter(
+ abha_number__abha_number=patient_data["healthIdNumber"]
+ ).first()
+
+ if not patient:
+ patient = PatientRegistration.objects.create(
+ facility=Facility.objects.get(external_id=counter_id),
+ name=patient_data["name"],
+ gender={"M": 1, "F": 2}.get(patient_data["gender"], 3),
+ is_antenatal=False,
+ phone_number=patient_data["mobile"],
+ emergency_phone_number=patient_data["mobile"],
+ date_of_birth=datetime.strptime(
+ f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}",
+ "%Y-%m-%d",
+ ).date(),
+ blood_group="UNK",
+ nationality="India",
+ address=patient_data["address"]["line"],
+ pincode=patient_data["address"]["pincode"],
+ )
+
+ abha_number = AbhaNumber.objects.create(
+ abha_number=patient_data["healthIdNumber"],
+ health_id=patient_data["healthId"],
+ name=patient_data["name"],
+ gender=patient_data["gender"],
+ date_of_birth=str(
+ datetime.strptime(
+ f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}",
+ "%Y-%m-%d",
+ )
+ )[0:10],
+ address=patient_data["address"]["line"],
+ district=patient_data["address"]["district"],
+ state=patient_data["address"]["state"],
+ pincode=patient_data["address"]["pincode"],
+ )
+
+ abha_number.save()
+ patient.abha_number = abha_number
+ patient.save()
+
+ self.get_linking_token(
+ {
+ "healthId": patient_data["healthId"]
+ or patient_data["healthIdNumber"],
+ "name": patient_data["name"],
+ "gender": patient_data["gender"],
+ "dateOfBirth": str(
+ datetime.strptime(
+ f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}",
+ "%Y-%m-%d",
+ )
+ )[0:10],
+ }
+ )
+
+ payload = {
+ "requestId": str(uuid.uuid4()),
+ "timestamp": str(
+ datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
+ ),
+ "acknowledgement": {
+ "status": "SUCCESS",
+ "healthId": patient_data["healthId"]
+ or patient_data["healthIdNumber"],
+ "tokenNumber": "100",
+ },
+ "error": None,
+ "resp": {
+ "requestId": data["requestId"],
+ },
+ }
+
+ on_share_response = AbdmGateway().on_share(payload)
+ if on_share_response.status_code == 202:
+ return Response(
+ on_share_response.request.body,
+ status=status.HTTP_202_ACCEPTED,
+ )
+
+ return Response(
+ {
+ "status": "ACCEPTED",
+ "healthId": patient_data["healthId"] or patient_data["healthIdNumber"],
+ },
+ status=status.HTTP_202_ACCEPTED,
+ )
diff --git a/care/abdm/api/viewsets/monitoring.py b/care/abdm/api/viewsets/monitoring.py
new file mode 100644
index 0000000000..54cfe30069
--- /dev/null
+++ b/care/abdm/api/viewsets/monitoring.py
@@ -0,0 +1,23 @@
+from datetime import datetime, timezone
+
+from rest_framework import status
+from rest_framework.generics import GenericAPIView
+from rest_framework.permissions import AllowAny
+from rest_framework.response import Response
+
+
+class HeartbeatView(GenericAPIView):
+ permission_classes = (AllowAny,)
+ authentication_classes = []
+
+ def get(self, request, *args, **kwargs):
+ return Response(
+ {
+ "timestamp": str(
+ datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
+ ),
+ "status": "UP",
+ "error": None,
+ },
+ status=status.HTTP_200_OK,
+ )
diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py
new file mode 100644
index 0000000000..8c126ec7ef
--- /dev/null
+++ b/care/abdm/api/viewsets/status.py
@@ -0,0 +1,36 @@
+from rest_framework import status
+from rest_framework.generics import GenericAPIView
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+
+from care.abdm.models import AbhaNumber
+from care.abdm.utils.api_call import AbdmGateway
+from care.facility.models.patient import PatientRegistration
+from config.authentication import ABDMAuthentication
+
+
+class NotifyView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ data = request.data
+
+ PatientRegistration.objects.filter(
+ abha_number__health_id=data["notification"]["patient"]["id"]
+ ).update(abha_number=None)
+ AbhaNumber.objects.filter(
+ health_id=data["notification"]["patient"]["id"]
+ ).delete()
+
+ AbdmGateway().patient_status_on_notify({"request_id": data["requestId"]})
+
+ return Response(status=status.HTTP_202_ACCEPTED)
+
+
+class SMSOnNotifyView(GenericAPIView):
+ permission_classes = (IsAuthenticated,)
+ authentication_classes = [ABDMAuthentication]
+
+ def post(self, request, *args, **kwargs):
+ return Response(status=status.HTTP_202_ACCEPTED)
diff --git a/care/abdm/apps.py b/care/abdm/apps.py
new file mode 100644
index 0000000000..54e278d631
--- /dev/null
+++ b/care/abdm/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+
+
+class AbdmConfig(AppConfig):
+ name = "care.abdm"
+ verbose_name = _("ABDM Integration")
diff --git a/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py b/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py
new file mode 100644
index 0000000000..ad5d70caa0
--- /dev/null
+++ b/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py
@@ -0,0 +1,69 @@
+# Generated by Django 4.2.2 on 2023-07-20 17:41
+
+import uuid
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ replaces = [
+ ("abdm", "0001_initial"),
+ ("abdm", "0002_auto_20221220_2312"),
+ ("abdm", "0003_auto_20221220_2321"),
+ ("abdm", "0004_auto_20221220_2325"),
+ ("abdm", "0005_auto_20221220_2327"),
+ ("abdm", "0006_auto_20230208_0915"),
+ ("abdm", "0007_alter_abhanumber_id"),
+ ]
+
+ dependencies = []
+
+ operations = [
+ migrations.CreateModel(
+ name="AbhaNumber",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ 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)),
+ ("abha_number", models.TextField(blank=True, null=True)),
+ ("email", models.EmailField(blank=True, max_length=254, null=True)),
+ ("first_name", models.TextField(blank=True, null=True)),
+ ("health_id", models.TextField(blank=True, null=True)),
+ ("last_name", models.TextField(blank=True, null=True)),
+ ("middle_name", models.TextField(blank=True, null=True)),
+ ("profile_photo", models.TextField(blank=True, null=True)),
+ ("txn_id", models.TextField(blank=True, null=True)),
+ ("access_token", models.TextField(blank=True, null=True)),
+ ("refresh_token", models.TextField(blank=True, null=True)),
+ ("address", models.TextField(blank=True, null=True)),
+ ("date_of_birth", models.TextField(blank=True, null=True)),
+ ("district", models.TextField(blank=True, null=True)),
+ ("gender", models.TextField(blank=True, null=True)),
+ ("name", models.TextField(blank=True, null=True)),
+ ("pincode", models.TextField(blank=True, null=True)),
+ ("state", models.TextField(blank=True, null=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ ]
diff --git a/care/abdm/migrations/__init__.py b/care/abdm/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/care/abdm/models.py b/care/abdm/models.py
new file mode 100644
index 0000000000..8b5abbfd65
--- /dev/null
+++ b/care/abdm/models.py
@@ -0,0 +1,35 @@
+# from django.db import models
+
+# Create your models here.
+
+from django.db import models
+
+from care.utils.models.base import BaseModel
+
+
+class AbhaNumber(BaseModel):
+ abha_number = models.TextField(null=True, blank=True)
+ health_id = models.TextField(null=True, blank=True)
+
+ name = models.TextField(null=True, blank=True)
+ first_name = models.TextField(null=True, blank=True)
+ middle_name = models.TextField(null=True, blank=True)
+ last_name = models.TextField(null=True, blank=True)
+
+ gender = models.TextField(null=True, blank=True)
+ date_of_birth = models.TextField(null=True, blank=True)
+
+ address = models.TextField(null=True, blank=True)
+ district = models.TextField(null=True, blank=True)
+ state = models.TextField(null=True, blank=True)
+ pincode = models.TextField(null=True, blank=True)
+
+ email = models.EmailField(null=True, blank=True)
+ profile_photo = models.TextField(null=True, blank=True)
+
+ txn_id = models.TextField(null=True, blank=True)
+ access_token = models.TextField(null=True, blank=True)
+ refresh_token = models.TextField(null=True, blank=True)
+
+ def __str__(self):
+ return self.abha_number
diff --git a/care/abdm/tests.py b/care/abdm/tests.py
new file mode 100644
index 0000000000..a79ca8be56
--- /dev/null
+++ b/care/abdm/tests.py
@@ -0,0 +1,3 @@
+# from django.test import TestCase
+
+# Create your tests here.
diff --git a/care/abdm/urls.py b/care/abdm/urls.py
new file mode 100644
index 0000000000..a6efbe58ea
--- /dev/null
+++ b/care/abdm/urls.py
@@ -0,0 +1,99 @@
+from django.urls import path
+from rest_framework.routers import SimpleRouter
+
+from care.abdm.api.viewsets.auth import (
+ AuthNotifyView,
+ DiscoverView,
+ LinkConfirmView,
+ LinkInitView,
+ NotifyView,
+ OnAddContextsView,
+ OnConfirmView,
+ OnFetchView,
+ OnInitView,
+ RequestDataView,
+)
+from care.abdm.api.viewsets.hip import HipViewSet
+from care.abdm.api.viewsets.monitoring import HeartbeatView
+from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView
+from care.abdm.api.viewsets.status import SMSOnNotifyView
+
+
+class OptionalSlashRouter(SimpleRouter):
+ def __init__(self):
+ super().__init__()
+ self.trailing_slash = "/?"
+
+
+abdm_router = OptionalSlashRouter()
+
+abdm_router.register("profile/v1.0/patients/", HipViewSet, basename="hip")
+
+abdm_urlpatterns = [
+ *abdm_router.urls,
+ path(
+ "v0.5/users/auth/on-fetch-modes",
+ OnFetchView.as_view(),
+ name="abdm_on_fetch_modes_view",
+ ),
+ path(
+ "v0.5/users/auth/on-init",
+ OnInitView.as_view(),
+ name="abdm_on_init_view",
+ ),
+ path(
+ "v0.5/users/auth/on-confirm",
+ OnConfirmView.as_view(),
+ name="abdm_on_confirm_view",
+ ),
+ path(
+ "v0.5/users/auth/notify",
+ AuthNotifyView.as_view(),
+ name="abdm_auth_notify_view",
+ ),
+ path(
+ "v0.5/links/link/on-add-contexts",
+ OnAddContextsView.as_view(),
+ name="abdm_on_add_context_view",
+ ),
+ path(
+ "v0.5/care-contexts/discover",
+ DiscoverView.as_view(),
+ name="abdm_discover_view",
+ ),
+ path(
+ "v0.5/links/link/init",
+ LinkInitView.as_view(),
+ name="abdm_link_init_view",
+ ),
+ path(
+ "v0.5/links/link/confirm",
+ LinkConfirmView.as_view(),
+ name="abdm_link_confirm_view",
+ ),
+ path(
+ "v0.5/consents/hip/notify",
+ NotifyView.as_view(),
+ name="abdm_notify_view",
+ ),
+ path(
+ "v0.5/health-information/hip/request",
+ RequestDataView.as_view(),
+ name="abdm_request_data_view",
+ ),
+ path(
+ "v0.5/patients/status/notify",
+ PatientStatusNotifyView.as_view(),
+ name="abdm_patient_status_notify_view",
+ ),
+ path(
+ "v0.5/patients/sms/on-notify",
+ SMSOnNotifyView.as_view(),
+ name="abdm_patient_status_notify_view",
+ ),
+ path(
+ "v0.5/heartbeat",
+ HeartbeatView.as_view(),
+ name="abdm_monitoring_heartbeat_view",
+ ),
+]
diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py
new file mode 100644
index 0000000000..4ff2a157ea
--- /dev/null
+++ b/care/abdm/utils/api_call.py
@@ -0,0 +1,768 @@
+import json
+import logging
+import uuid
+from base64 import b64encode
+from datetime import datetime, timedelta, timezone
+
+import requests
+from Crypto.Cipher import PKCS1_v1_5
+from Crypto.PublicKey import RSA
+from django.conf import settings
+from django.core.cache import cache
+from django.db.models import Q
+
+from care.abdm.models import AbhaNumber
+from care.facility.models.patient_consultation import PatientConsultation
+
+GATEWAY_API_URL = settings.ABDM_URL
+HEALTH_SERVICE_API_URL = settings.HEALTH_SERVICE_API_URL
+ABDM_GATEWAY_URL = GATEWAY_API_URL + "/gateway"
+ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions"
+ABDM_TOKEN_CACHE_KEY = "abdm_token"
+
+# TODO: Exception handling for all api calls, need to gracefully handle known exceptions
+
+logger = logging.getLogger(__name__)
+
+
+def encrypt_with_public_key(a_message):
+ rsa_public_key = RSA.importKey(
+ requests.get(HEALTH_SERVICE_API_URL + "/v2/auth/cert").text.strip()
+ )
+ rsa_public_key = PKCS1_v1_5.new(rsa_public_key)
+ encrypted_text = rsa_public_key.encrypt(a_message.encode())
+ return b64encode(encrypted_text).decode()
+
+
+class APIGateway:
+ def __init__(self, gateway, token):
+ if gateway == "health":
+ self.url = HEALTH_SERVICE_API_URL
+ elif gateway == "abdm":
+ self.url = GATEWAY_API_URL
+ elif gateway == "abdm_gateway":
+ self.url = ABDM_GATEWAY_URL
+ else:
+ self.url = GATEWAY_API_URL
+ self.token = token
+
+ # def encrypt(self, data):
+ # cert = cache.get("abdm_cert")
+ # if not cert:
+ # cert = requests.get(settings.ABDM_CERT_URL).text
+ # cache.set("abdm_cert", cert, 3600)
+
+ def add_user_header(self, headers, user_token):
+ headers.update(
+ {
+ "X-Token": "Bearer " + user_token,
+ }
+ )
+ return headers
+
+ def add_auth_header(self, headers):
+ token = cache.get(ABDM_TOKEN_CACHE_KEY)
+ if not token:
+ logger.info("No Token in Cache")
+ data = {
+ "clientId": settings.ABDM_CLIENT_ID,
+ "clientSecret": settings.ABDM_CLIENT_SECRET,
+ }
+ auth_headers = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ }
+ resp = requests.post(
+ ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers
+ )
+ logger.info("Token Response Status: {}".format(resp.status_code))
+ if resp.status_code < 300:
+ # Checking if Content-Type is application/json
+ if resp.headers["Content-Type"] != "application/json":
+ logger.info(
+ "Unsupported Content-Type: {}".format(
+ resp.headers["Content-Type"]
+ )
+ )
+ logger.info("Response: {}".format(resp.text))
+ return None
+ else:
+ data = resp.json()
+ token = data["accessToken"]
+ expires_in = data["expiresIn"]
+ logger.info("New Token: {}".format(token))
+ logger.info("Expires in: {}".format(expires_in))
+ cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in)
+ else:
+ logger.info("Bad Response: {}".format(resp.text))
+ return None
+ # logger.info("Returning Authorization Header: Bearer {}".format(token))
+ logger.info("Adding Authorization Header")
+ auth_header = {"Authorization": "Bearer {}".format(token)}
+ return {**headers, **auth_header}
+
+ def add_additional_headers(self, headers, additional_headers):
+ return {**headers, **additional_headers}
+
+ def get(self, path, params=None, auth=None):
+ url = self.url + path
+ headers = {}
+ headers = self.add_auth_header(headers)
+ if auth:
+ headers = self.add_user_header(headers, auth)
+ logger.info("Making GET Request to: {}".format(url))
+ response = requests.get(url, headers=headers, params=params)
+ logger.info("{} Response: {}".format(response.status_code, response.text))
+ return response
+
+ def post(self, path, data=None, auth=None, additional_headers=None):
+ url = self.url + path
+ headers = {
+ "Content-Type": "application/json",
+ "accept": "*/*",
+ "Accept-Language": "en-US",
+ }
+ headers = self.add_auth_header(headers)
+ if auth:
+ headers = self.add_user_header(headers, auth)
+ if additional_headers:
+ headers = self.add_additional_headers(headers, additional_headers)
+ # headers_string = " ".join(
+ # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()]
+ # )
+ data_json = json.dumps(data)
+ # logger.info("curl -X POST {} {} -d {}".format(url, headers_string, data_json))
+ logger.info("Posting Request to: {}".format(url))
+ response = requests.post(url, headers=headers, data=data_json)
+ logger.info("{} Response: {}".format(response.status_code, response.text))
+ return response
+
+
+class HealthIdGateway:
+ def __init__(self):
+ self.api = APIGateway("health", None)
+
+ def generate_aadhaar_otp(self, data):
+ path = "/v1/registration/aadhaar/generateOtp"
+ response = self.api.post(path, data)
+ logger.info("{} Response: {}".format(response.status_code, response.text))
+ return response.json()
+
+ def resend_aadhaar_otp(self, data):
+ path = "/v1/registration/aadhaar/resendAadhaarOtp"
+ response = self.api.post(path, data)
+ return response.json()
+
+ def verify_aadhaar_otp(self, data):
+ path = "/v1/registration/aadhaar/verifyOTP"
+ response = self.api.post(path, data)
+ return response.json()
+
+ def check_and_generate_mobile_otp(self, data):
+ path = "/v2/registration/aadhaar/checkAndGenerateMobileOTP"
+ response = self.api.post(path, data)
+ return response.json()
+
+ def generate_mobile_otp(self, data):
+ path = "/v2/registration/aadhaar/generateMobileOTP"
+ response = self.api.post(path, data)
+ return response.json()
+
+ # /v1/registration/aadhaar/verifyMobileOTP
+ def verify_mobile_otp(self, data):
+ path = "/v1/registration/aadhaar/verifyMobileOTP"
+ response = self.api.post(path, data)
+ return response.json()
+
+ # /v1/registration/aadhaar/createHealthIdWithPreVerified
+ def create_health_id(self, data):
+ path = "/v1/registration/aadhaar/createHealthIdWithPreVerified"
+ logger.info("Creating Health ID with data: {}".format(data))
+ # data.pop("healthId", None)
+ response = self.api.post(path, data)
+ return response.json()
+
+ # /v1/search/existsByHealthId
+ # API checks if ABHA Address/ABHA Number is reserved/used which includes permanently deleted ABHA Addresses
+ # Return { status: true }
+ def exists_by_health_id(self, data):
+ path = "/v1/search/existsByHealthId"
+ response = self.api.post(path, data)
+ return response.json()
+
+ # /v1/search/searchByHealthId
+ # API returns only Active or Deactive ABHA Number/ Address (Never returns Permanently Deleted ABHA Number/Address)
+ # Returns {
+ # "authMethods": [
+ # "AADHAAR_OTP"
+ # ],
+ # "healthId": "deepakndhm",
+ # "healthIdNumber": "43-4221-5105-6749",
+ # "name": "kishan kumar singh",
+ # "status": "ACTIVE"
+ # }
+ def search_by_health_id(self, data):
+ path = "/v1/search/searchByHealthId"
+ response = self.api.post(path, data)
+ return response.json()
+
+ # /v1/search/searchByMobile
+ def search_by_mobile(self, data):
+ path = "/v1/search/searchByMobile"
+ response = self.api.post(path, data)
+ return response.json()
+
+ # Auth APIs
+
+ # /v1/auth/init
+ def auth_init(self, data):
+ path = "/v1/auth/init"
+ response = self.api.post(path, data)
+ return response.json()
+
+ # /v1/auth/confirmWithAadhaarOtp
+ def confirm_with_aadhaar_otp(self, data):
+ path = "/v1/auth/confirmWithAadhaarOtp"
+ response = self.api.post(path, data)
+ return response.json()
+
+ # /v1/auth/confirmWithMobileOTP
+ def confirm_with_mobile_otp(self, data):
+ path = "/v1/auth/confirmWithMobileOTP"
+ response = self.api.post(path, data)
+ return response.json()
+
+ # /v1/auth/confirmWithDemographics
+ def confirm_with_demographics(self, data):
+ path = "/v1/auth/confirmWithDemographics"
+ response = self.api.post(path, data)
+ return response.json()
+
+ def verify_demographics(self, health_id, name, gender, year_of_birth):
+ auth_init_response = HealthIdGateway().auth_init(
+ {"authMethod": "DEMOGRAPHICS", "healthid": health_id}
+ )
+ if "txnId" in auth_init_response:
+ demographics_response = HealthIdGateway().confirm_with_demographics(
+ {
+ "txnId": auth_init_response["txnId"],
+ "name": name,
+ "gender": gender,
+ "yearOfBirth": year_of_birth,
+ }
+ )
+ return "status" in demographics_response and demographics_response["status"]
+
+ return False
+
+ # /v1/auth/generate/access-token
+ def generate_access_token(self, data):
+ if "access_token" in data:
+ return data["access_token"]
+ elif "accessToken" in data:
+ return data["accessToken"]
+ elif "token" in data:
+ return data["token"]
+
+ if "refreshToken" in data:
+ refreshToken = data["refreshToken"]
+ elif "refresh_token" in data:
+ refreshToken = data["refresh_token"]
+ else:
+ return None
+ path = "/v1/auth/generate/access-token"
+ response = self.api.post(path, {"refreshToken": refreshToken})
+ return response.json()["accessToken"]
+
+ # Account APIs
+
+ # /v1/account/profile
+ def get_profile(self, data):
+ path = "/v1/account/profile"
+ access_token = self.generate_access_token(data)
+ response = self.api.get(path, {}, access_token)
+ return response.json()
+
+ # /v1/account/getPngCard
+ def get_abha_card_png(self, data):
+ path = "/v1/account/getPngCard"
+ access_token = self.generate_access_token(data)
+ response = self.api.get(path, {}, access_token)
+
+ return b64encode(response.content)
+
+ def get_abha_card_pdf(self, data):
+ path = "/v1/account/getCard"
+ access_token = self.generate_access_token(data)
+ response = self.api.get(path, {}, access_token)
+
+ return b64encode(response.content)
+
+ # /v1/account/qrCode
+ def get_qr_code(self, data, auth):
+ path = "/v1/account/qrCode"
+ access_token = self.generate_access_token(data)
+ logger.info("Getting QR Code for: {}".format(data))
+ response = self.api.get(path, {}, access_token)
+ logger.info("QR Code Response: {}".format(response.text))
+ return response.json()
+
+
+class HealthIdGatewayV2:
+ def __init__(self):
+ self.api = APIGateway("health", None)
+
+ # V2 APIs
+ def generate_aadhaar_otp(self, data):
+ path = "/v2/registration/aadhaar/generateOtp"
+ data["aadhaar"] = encrypt_with_public_key(data["aadhaar"])
+ data.pop("cancelToken", {})
+ response = self.api.post(path, data)
+ return response.json()
+
+ def generate_document_mobile_otp(self, data):
+ path = "/v2/document/generate/mobile/otp"
+ data["mobile"] = "ENTER MOBILE NUMBER HERE" # Hard Coding for test
+ data.pop("cancelToken", {})
+ response = self.api.post(path, data)
+ return response.json()
+
+ def verify_document_mobile_otp(self, data):
+ path = "/v2/document/verify/mobile/otp"
+ data["otp"] = encrypt_with_public_key(data["otp"])
+ data.pop("cancelToken", {})
+ response = self.api.post(path, data)
+ return response.json()
+
+
+class AbdmGateway:
+ # TODO: replace this with in-memory db (redis)
+ temp_memory = {}
+ hip_name = "Coronasafe Care 01"
+ hip_id = "IN3210000017"
+
+ def __init__(self):
+ self.api = APIGateway("abdm_gateway", None)
+
+ def add_care_context(self, access_token, request_id):
+ if request_id not in self.temp_memory:
+ return
+
+ data = self.temp_memory[request_id]
+
+ if "consultationId" in data:
+ consultation = PatientConsultation.objects.get(
+ external_id=data["consultationId"]
+ )
+
+ response = self.add_contexts(
+ {
+ "access_token": access_token,
+ "patient_id": str(consultation.patient.external_id),
+ "patient_name": consultation.patient.name,
+ "context_id": str(consultation.external_id),
+ "context_name": f"Encounter: {str(consultation.created_date.date())}",
+ }
+ )
+
+ return response
+
+ return False
+
+ def save_linking_token(self, patient, access_token, request_id):
+ if request_id not in self.temp_memory:
+ return
+
+ data = self.temp_memory[request_id]
+ health_id = patient and patient["id"] or data["healthId"]
+
+ abha_object = AbhaNumber.objects.filter(
+ Q(abha_number=health_id) | Q(health_id=health_id)
+ ).first()
+
+ if abha_object:
+ abha_object.access_token = access_token
+ abha_object.save()
+ return True
+
+ return False
+
+ # /v0.5/users/auth/fetch-modes
+ def fetch_modes(self, data):
+ path = "/v0.5/users/auth/fetch-modes"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+ request_id = str(uuid.uuid4())
+
+ """
+ data = {
+ healthId,
+ name,
+ gender,
+ dateOfBirth,
+ }
+ """
+ self.temp_memory[request_id] = data
+
+ if "authMode" in data and data["authMode"] == "DIRECT":
+ self.init(request_id)
+ return
+
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "query": {
+ "id": data["healthId"],
+ "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK",
+ "requester": {"type": "HIP", "id": self.hip_id},
+ },
+ }
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ # "/v0.5/users/auth/init"
+ def init(self, prev_request_id):
+ if prev_request_id not in self.temp_memory:
+ return
+
+ path = "/v0.5/users/auth/init"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+
+ data = self.temp_memory[prev_request_id]
+ self.temp_memory[request_id] = data
+
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "query": {
+ "id": data["healthId"],
+ "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK",
+ "authMode": data["authMode"] if "authMode" in data else "DEMOGRAPHICS",
+ "requester": {"type": "HIP", "id": self.hip_id},
+ },
+ }
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ # "/v0.5/users/auth/confirm"
+ def confirm(self, transaction_id, prev_request_id):
+ if prev_request_id not in self.temp_memory:
+ return
+
+ path = "/v0.5/users/auth/confirm"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+
+ data = self.temp_memory[prev_request_id]
+ self.temp_memory[request_id] = data
+
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "transactionId": transaction_id,
+ "credential": {
+ "demographic": {
+ "name": data["name"],
+ "gender": data["gender"],
+ "dateOfBirth": data["dateOfBirth"],
+ },
+ "authCode": "",
+ },
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def auth_on_notify(self, data):
+ path = "/v0.5/links/link/on-init"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "acknowledgement": {"status": "OK"},
+ # "error": {"code": 1000, "message": "string"},
+ "resp": {"requestId": data["request_id"]},
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ # TODO: make it dynamic and call it at discharge (call it from on_confirm)
+ def add_contexts(self, data):
+ path = "/v0.5/links/link/add-contexts"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "link": {
+ "accessToken": data["access_token"],
+ "patient": {
+ "referenceNumber": data["patient_id"],
+ "display": data["patient_name"],
+ "careContexts": [
+ {
+ "referenceNumber": data["context_id"],
+ "display": data["context_name"],
+ }
+ ],
+ },
+ },
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def on_discover(self, data):
+ path = "/v0.5/care-contexts/on-discover"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "transactionId": data["transaction_id"],
+ "patient": {
+ "referenceNumber": data["patient_id"],
+ "display": data["patient_name"],
+ "careContexts": list(
+ map(
+ lambda context: {
+ "referenceNumber": context["id"],
+ "display": context["name"],
+ },
+ data["care_contexts"],
+ )
+ ),
+ "matchedBy": data["matched_by"],
+ },
+ # "error": {"code": 1000, "message": "string"},
+ "resp": {"requestId": data["request_id"]},
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def on_link_init(self, data):
+ path = "/v0.5/links/link/on-init"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "transactionId": data["transaction_id"],
+ "link": {
+ "referenceNumber": data["patient_id"],
+ "authenticationType": "DIRECT",
+ "meta": {
+ "communicationMedium": "MOBILE",
+ "communicationHint": data["phone"],
+ "communicationExpiry": str(
+ (datetime.now() + timedelta(minutes=15)).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ )
+ ),
+ },
+ },
+ # "error": {"code": 1000, "message": "string"},
+ "resp": {"requestId": data["request_id"]},
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def on_link_confirm(self, data):
+ path = "/v0.5/links/link/on-confirm"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "patient": {
+ "referenceNumber": data["patient_id"],
+ "display": data["patient_name"],
+ "careContexts": list(
+ map(
+ lambda context: {
+ "referenceNumber": context["id"],
+ "display": context["name"],
+ },
+ data["care_contexts"],
+ )
+ ),
+ },
+ # "error": {"code": 1000, "message": "string"},
+ "resp": {"requestId": data["request_id"]},
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def on_notify(self, data):
+ path = "/v0.5/consents/hip/on-notify"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "acknowledgement": {"status": "OK", "consentId": data["consent_id"]},
+ # "error": {"code": 1000, "message": "string"},
+ "resp": {"requestId": data["request_id"]},
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def on_data_request(self, data):
+ path = "/v0.5/health-information/hip/on-request"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "hiRequest": {
+ "transactionId": data["transaction_id"],
+ "sessionStatus": "ACKNOWLEDGED",
+ },
+ # "error": {"code": 1000, "message": "string"},
+ "resp": {"requestId": data["request_id"]},
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def data_transfer(self, data):
+ headers = {"Content-Type": "application/json"}
+
+ payload = {
+ "pageNumber": 1,
+ "pageCount": 1,
+ "transactionId": data["transaction_id"],
+ "entries": list(
+ map(
+ lambda context: {
+ "content": context["data"],
+ "media": "application/fhir+json",
+ "checksum": "string",
+ "careContextReference": context["consultation_id"],
+ },
+ data["care_contexts"],
+ )
+ ),
+ "keyMaterial": data["key_material"],
+ }
+
+ response = requests.post(
+ data["data_push_url"], data=json.dumps(payload), headers=headers
+ )
+ return response
+
+ def data_notify(self, data):
+ path = "/v0.5/health-information/notify"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "notification": {
+ "consentId": data["consent_id"],
+ "transactionId": data["transaction_id"],
+ "doneAt": str(
+ datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
+ ),
+ "statusNotification": {
+ "sessionStatus": "TRANSFERRED",
+ "hipId": self.hip_id,
+ "statusResponses": list(
+ map(
+ lambda context: {
+ "careContextReference": context["id"],
+ "hiStatus": "OK",
+ "description": "success", # not sure what to put
+ },
+ data["care_contexts"],
+ )
+ ),
+ },
+ },
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def patient_status_on_notify(self, data):
+ path = "/v0.5/patients/status/on-notify"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "acknowledgement": {"status": "OK"},
+ # "error": {"code": 1000, "message": "string"},
+ "resp": {"requestId": data["request_id"]},
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ def patient_sms_notify(self, data):
+ path = "/v0.5/patients/sms/notify2"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+
+ request_id = str(uuid.uuid4())
+ payload = {
+ "requestId": request_id,
+ "timestamp": datetime.now(tz=timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%S.000Z"
+ ),
+ "notification": {
+ "phoneNo": f"+91-{data['phone']}",
+ "hip": {"name": self.hip_name, "id": self.hip_id},
+ },
+ }
+
+ response = self.api.post(path, payload, None, additional_headers)
+ return response
+
+ # /v1.0/patients/profile/on-share
+ def on_share(self, data):
+ path = "/v1.0/patients/profile/on-share"
+ additional_headers = {"X-CM-ID": settings.X_CM_ID}
+ response = self.api.post(path, data, None, additional_headers)
+ return response
diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py
new file mode 100644
index 0000000000..2401d1e1ed
--- /dev/null
+++ b/care/abdm/utils/cipher.py
@@ -0,0 +1,66 @@
+import json
+
+import requests
+from django.conf import settings
+
+
+class Cipher:
+ server_url = settings.FIDELIUS_URL
+
+ def __init__(self, reciever_public_key, reciever_nonce):
+ self.reciever_public_key = reciever_public_key
+ self.reciever_nonce = reciever_nonce
+
+ self.sender_private_key = None
+ self.sender_public_key = None
+ self.sender_nonce = None
+
+ self.key_to_share = None
+
+ def generate_key_pair(self):
+ response = requests.get(f"{self.server_url}/keys/generate")
+
+ if response.status_code == 200:
+ key_material = response.json()
+
+ self.sender_private_key = key_material["privateKey"]
+ self.sender_public_key = key_material["publicKey"]
+ self.sender_nonce = key_material["nonce"]
+
+ return key_material
+
+ return None
+
+ def encrypt(self, paylaod):
+ if not self.sender_private_key:
+ key_material = self.generate_key_pair()
+
+ if not key_material:
+ return None
+
+ response = requests.post(
+ f"{self.server_url}/encrypt",
+ headers={"Content-Type": "application/json"},
+ data=json.dumps(
+ {
+ "receiverPublicKey": self.reciever_public_key,
+ "receiverNonce": self.reciever_nonce,
+ "senderPrivateKey": self.sender_private_key,
+ "senderPublicKey": self.sender_public_key,
+ "senderNonce": self.sender_nonce,
+ "plainTextData": paylaod,
+ }
+ ),
+ )
+
+ if response.status_code == 200:
+ data = response.json()
+ self.key_to_share = data["keyToShare"]
+
+ return {
+ "public_key": self.key_to_share,
+ "data": data["encryptedData"],
+ "nonce": self.sender_nonce,
+ }
+
+ return None
diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py
new file mode 100644
index 0000000000..b28ab3eb70
--- /dev/null
+++ b/care/abdm/utils/fhir.py
@@ -0,0 +1,1201 @@
+import base64
+from datetime import datetime, timezone
+from uuid import uuid4 as uuid
+
+from fhir.resources.address import Address
+from fhir.resources.annotation import Annotation
+from fhir.resources.attachment import Attachment
+from fhir.resources.bundle import Bundle, BundleEntry
+from fhir.resources.careplan import CarePlan
+from fhir.resources.codeableconcept import CodeableConcept
+from fhir.resources.coding import Coding
+from fhir.resources.composition import Composition, CompositionSection
+from fhir.resources.condition import Condition
+from fhir.resources.contactpoint import ContactPoint
+from fhir.resources.diagnosticreport import DiagnosticReport
+from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent
+from fhir.resources.dosage import Dosage
+from fhir.resources.encounter import Encounter, EncounterDiagnosis
+from fhir.resources.humanname import HumanName
+from fhir.resources.identifier import Identifier
+from fhir.resources.immunization import Immunization, ImmunizationProtocolApplied
+from fhir.resources.medication import Medication
+from fhir.resources.medicationrequest import MedicationRequest
+from fhir.resources.meta import Meta
+from fhir.resources.observation import Observation, ObservationComponent
+from fhir.resources.organization import Organization
+from fhir.resources.patient import Patient
+from fhir.resources.period import Period
+from fhir.resources.practitioner import Practitioner
+from fhir.resources.procedure import Procedure
+from fhir.resources.quantity import Quantity
+from fhir.resources.reference import Reference
+
+from care.facility.models.file_upload import FileUpload
+from care.facility.models.patient_investigation import InvestigationValue
+from care.facility.static_data.icd11 import ICDDiseases
+
+
+class Fhir:
+ def __init__(self, consultation):
+ self.consultation = consultation
+
+ self._patient_profile = None
+ self._practitioner_profile = None
+ self._organization_profile = None
+ self._encounter_profile = None
+ self._careplan_profile = None
+ self._diagnostic_report_profile = None
+ self._immunization_profile = None
+ self._medication_profiles = []
+ self._medication_request_profiles = []
+ self._observation_profiles = []
+ self._document_reference_profiles = []
+ self._condition_profiles = []
+ self._procedure_profiles = []
+
+ def _reference_url(self, resource=None):
+ if resource is None:
+ return ""
+
+ return f"{resource.resource_type}/{resource.id}"
+
+ def _reference(self, resource=None):
+ if resource is None:
+ return None
+
+ return Reference(reference=self._reference_url(resource))
+
+ def _patient(self):
+ if self._patient_profile is not None:
+ return self._patient_profile
+
+ id = str(self.consultation.patient.external_id)
+ name = self.consultation.patient.name
+ gender = self.consultation.patient.gender
+ self._patient_profile = Patient(
+ id=id,
+ identifier=[Identifier(value=id)],
+ name=[HumanName(text=name)],
+ gender="male" if gender == 1 else "female" if gender == 2 else "other",
+ )
+
+ return self._patient_profile
+
+ def _practioner(self):
+ if self._practitioner_profile is not None:
+ return self._practitioner_profile
+
+ id = str(uuid())
+ name = (
+ self.consultation.verified_by
+ or f"{self.consultation.created_by.first_name} {self.consultation.created_by.last_name}"
+ )
+ self._practitioner_profile = Practitioner(
+ id=id,
+ identifier=[Identifier(value=id)],
+ name=[HumanName(text=name)],
+ )
+
+ return self._practitioner_profile
+
+ def _organization(self):
+ if self._organization_profile is not None:
+ return self._organization_profile
+
+ id = str(self.consultation.facility.external_id)
+ hip_id = "IN3210000017" # TODO: make it dynamic
+ name = self.consultation.facility.name
+ phone = self.consultation.facility.phone_number
+ address = self.consultation.facility.address
+ local_body = self.consultation.facility.local_body.name
+ district = self.consultation.facility.district.name
+ state = self.consultation.facility.state.name
+ pincode = self.consultation.facility.pincode
+ self._organization_profile = Organization(
+ id=id,
+ identifier=[
+ Identifier(system="https://facilitysbx.ndhm.gov.in", value=hip_id)
+ ],
+ name=name,
+ telecom=[ContactPoint(system="phone", value=phone)],
+ address=[
+ Address(
+ line=[address, local_body],
+ district=district,
+ state=state,
+ postalCode=pincode,
+ country="INDIA",
+ )
+ ],
+ )
+
+ return self._organization_profile
+
+ def _condition(self, diagnosis_id, provisional=False):
+ diagnosis = ICDDiseases.by.id[diagnosis_id]
+ [code, label] = diagnosis.label.split(" ", 1)
+ condition_profile = Condition(
+ id=diagnosis_id,
+ identifier=[Identifier(value=diagnosis_id)],
+ category=[
+ CodeableConcept(
+ coding=[
+ Coding(
+ system="http://terminology.hl7.org/CodeSystem/condition-category",
+ code="encounter-diagnosis",
+ display="Encounter Diagnosis",
+ )
+ ],
+ text="Encounter Diagnosis",
+ )
+ ],
+ verificationStatus=CodeableConcept(
+ coding=[
+ Coding(
+ system="http://terminology.hl7.org/CodeSystem/condition-ver-status",
+ code="provisional" if provisional else "confirmed",
+ display="Provisional" if provisional else "Confirmed",
+ )
+ ]
+ ),
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="http://id.who.int/icd/release/11/mms",
+ code=code,
+ display=label,
+ )
+ ],
+ text=diagnosis.label,
+ ),
+ subject=self._reference(self._patient()),
+ )
+
+ self._condition_profiles.append(condition_profile)
+ return condition_profile
+
+ def _procedure(self, procedure):
+ procedure_profile = Procedure(
+ id=str(uuid()),
+ status="completed",
+ code=CodeableConcept(
+ text=procedure["procedure"],
+ ),
+ subject=self._reference(self._patient()),
+ performedDateTime=f"{procedure['time']}:00+05:30"
+ if not procedure["repetitive"]
+ else None,
+ performedString=f"Every {procedure['frequency']}"
+ if procedure["repetitive"]
+ else None,
+ )
+
+ self._procedure_profiles.append(procedure_profile)
+ return procedure_profile
+
+ def _careplan(self):
+ if self._careplan_profile:
+ return self._careplan_profile
+
+ self._careplan_profile = CarePlan(
+ id=str(uuid()),
+ status="completed",
+ intent="plan",
+ title="Care Plan",
+ description="This includes Treatment Summary, Prescribed Medication, General Notes and Special Instructions",
+ period=Period(
+ start=self.consultation.admission_date.isoformat(),
+ end=self.consultation.discharge_date.isoformat()
+ if self.consultation.discharge_date
+ else None,
+ ),
+ note=[
+ Annotation(text=self.consultation.prescribed_medication),
+ Annotation(text=self.consultation.consultation_notes),
+ Annotation(text=self.consultation.special_instruction),
+ ],
+ subject=self._reference(self._patient()),
+ )
+
+ return self._careplan_profile
+
+ def _diagnostic_report(self):
+ if self._diagnostic_report_profile:
+ return self._diagnostic_report_profile
+
+ self._diagnostic_report_profile = DiagnosticReport(
+ id=str(uuid()),
+ status="final",
+ code=CodeableConcept(text="Investigation/Test Results"),
+ result=list(
+ map(
+ lambda investigation: self._reference(
+ self._observation(
+ title=investigation.investigation.name,
+ value={
+ "value": investigation.value,
+ "unit": investigation.investigation.unit,
+ },
+ id=str(investigation.external_id),
+ date=investigation.created_date.isoformat(),
+ )
+ ),
+ InvestigationValue.objects.filter(consultation=self.consultation),
+ )
+ ),
+ subject=self._reference(self._patient()),
+ performer=[self._reference(self._organization())],
+ resultsInterpreter=[self._reference(self._organization())],
+ conclusion="Refer to Doctor. To be correlated with further study.",
+ )
+
+ return self._diagnostic_report_profile
+
+ def _observation(self, title, value, id, date):
+ if not value or (type(value) == dict and not value["value"]):
+ return
+
+ return Observation(
+ id=f"{id}.{title.replace(' ', '').replace('_', '-')}"
+ if id and title
+ else str(uuid()),
+ status="final",
+ effectiveDateTime=date if date else None,
+ code=CodeableConcept(text=title),
+ valueQuantity=Quantity(value=str(value["value"]), unit=value["unit"])
+ if type(value) == dict
+ else None,
+ valueString=value if type(value) == str else None,
+ component=list(
+ map(
+ lambda component: ObservationComponent(
+ code=CodeableConcept(text=component["title"]),
+ valueQuantity=Quantity(
+ value=component["value"], unit=component["unit"]
+ )
+ if type(component) == dict
+ else None,
+ valueString=component if type(component) == str else None,
+ ),
+ value,
+ )
+ )
+ if type(value) == list
+ else None,
+ )
+
+ def _observations_from_daily_round(self, daily_round):
+ id = str(daily_round.external_id)
+ date = daily_round.created_date.isoformat()
+ observation_profiles = [
+ self._observation(
+ "Temperature",
+ {"value": daily_round.temperature, "unit": "F"},
+ id,
+ date,
+ ),
+ self._observation(
+ "SpO2",
+ {"value": daily_round.spo2, "unit": "%"},
+ id,
+ date,
+ ),
+ self._observation(
+ "Pulse",
+ {"value": daily_round.pulse, "unit": "bpm"},
+ id,
+ date,
+ ),
+ self._observation(
+ "Resp",
+ {"value": daily_round.resp, "unit": "bpm"},
+ id,
+ date,
+ ),
+ self._observation(
+ "Blood Pressure",
+ [
+ {
+ "title": "Systolic Blood Pressure",
+ "value": daily_round.bp["systolic"],
+ "unit": "mmHg",
+ },
+ {
+ "title": "Diastolic Blood Pressure",
+ "value": daily_round.bp["diastolic"],
+ "unit": "mmHg",
+ },
+ ]
+ if "systolic" in daily_round.bp and "diastolic" in daily_round.bp
+ else None,
+ id,
+ date,
+ ),
+ ]
+
+ # TODO: do it for other fields like bp, pulse, spo2, ...
+
+ observation_profiles = list(
+ filter(lambda profile: profile is not None, observation_profiles)
+ )
+ self._observation_profiles.extend(observation_profiles)
+ return observation_profiles
+
+ def _encounter(self, include_diagnosis=False):
+ if self._encounter_profile is not None:
+ return self._encounter_profile
+
+ id = str(self.consultation.external_id)
+ status = "finished" if self.consultation.discharge_date else "in-progress"
+ period_start = self.consultation.admission_date.isoformat()
+ period_end = (
+ self.consultation.discharge_date.isoformat()
+ if self.consultation.discharge_date
+ else None
+ )
+ self._encounter_profile = Encounter(
+ **{
+ "id": id,
+ "identifier": [Identifier(value=id)],
+ "status": status,
+ "class": Coding(code="IMP", display="Inpatient Encounter"),
+ "subject": self._reference(self._patient()),
+ "period": Period(start=period_start, end=period_end),
+ "diagnosis": list(
+ map(
+ lambda diagnosis: EncounterDiagnosis(
+ condition=self._reference(
+ self._condition(diagnosis),
+ )
+ ),
+ self.consultation.icd11_diagnoses,
+ )
+ )
+ + list(
+ map(
+ lambda diagnosis: EncounterDiagnosis(
+ condition=self._reference(self._condition(diagnosis))
+ ),
+ self.consultation.icd11_provisional_diagnoses,
+ )
+ )
+ if include_diagnosis
+ else None,
+ }
+ )
+
+ return self._encounter_profile
+
+ def _immunization(self):
+ if self._immunization_profile:
+ return self._immunization_profile
+
+ if not self.consultation.patient.is_vaccinated:
+ return
+
+ self._immunization_profile = Immunization(
+ id=str(uuid()),
+ status="completed",
+ identifier=[
+ Identifier(
+ type=CodeableConcept(text="Covin Id"),
+ value=self.consultation.patient.covin_id,
+ )
+ ],
+ vaccineCode=CodeableConcept(
+ coding=[
+ Coding(
+ system="http://snomed.info/sct",
+ code="1119305005",
+ display="COVID-19 antigen vaccine",
+ )
+ ],
+ text=self.consultation.patient.vaccine_name,
+ ),
+ patient=self._reference(self._patient()),
+ route=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="47625008",
+ display="Intravenous route",
+ )
+ ]
+ ),
+ occurrenceDateTime=self.consultation.patient.last_vaccinated_date.isoformat(),
+ protocolApplied=[
+ ImmunizationProtocolApplied(
+ doseNumberPositiveInt=self.consultation.patient.number_of_doses
+ )
+ ],
+ )
+
+ def _document_reference(self, file):
+ id = str(file.external_id)
+ content_type, content = file.file_contents()
+ document_reference_profile = DocumentReference(
+ id=id,
+ identifier=[Identifier(value=id)],
+ status="current",
+ type=CodeableConcept(text=file.internal_name.split(".")[0]),
+ content=[
+ DocumentReferenceContent(
+ attachment=Attachment(
+ contentType=content_type, data=base64.b64encode(content)
+ )
+ )
+ ],
+ author=[self._reference(self._organization())],
+ )
+
+ self._document_reference_profiles.append(document_reference_profile)
+ return document_reference_profile
+
+ def _medication(self, name):
+ medication_profile = Medication(id=str(uuid()), code=CodeableConcept(text=name))
+
+ self._medication_profiles.append(medication_profile)
+ return medication_profile
+
+ def _medication_request(self, medicine):
+ id = str(uuid())
+ prescription_date = (
+ self.consultation.admission_date.isoformat()
+ ) # TODO: change to the time of prescription
+ status = "unknown" # TODO: get correct status active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown
+ dosage_text = f"{medicine['dosage_new']} / {medicine['dosage']} for {medicine['days']} days"
+
+ medication_profile = self._medication(medicine["medicine"])
+ medication_request_profile = MedicationRequest(
+ id=id,
+ identifier=[Identifier(value=id)],
+ status=status,
+ intent="order",
+ authoredOn=prescription_date,
+ dosageInstruction=[Dosage(text=dosage_text)],
+ medicationReference=self._reference(medication_profile),
+ subject=self._reference(self._patient()),
+ requester=self._reference(self._practioner()),
+ )
+
+ self._medication_request_profiles.append(medication_request_profile)
+ return medication_profile, medication_request_profile
+
+ def _prescription_composition(self):
+ id = str(uuid()) # TODO: use identifiable id
+ return Composition(
+ id=id,
+ identifier=Identifier(value=id),
+ status="final", # TODO: use appropriate one
+ type=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="440545006",
+ display="Prescription record",
+ )
+ ]
+ ),
+ title="Prescription",
+ date=datetime.now(timezone.utc).isoformat(),
+ section=[
+ CompositionSection(
+ title="In Patient Prescriptions",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="440545006",
+ display="Prescription record",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda medicine: self._reference(
+ self._medication_request(medicine)[1]
+ ),
+ self.consultation.discharge_advice,
+ )
+ ),
+ )
+ ],
+ subject=self._reference(self._patient()),
+ encounter=self._reference(self._encounter()),
+ author=[self._reference(self._organization())],
+ )
+
+ def _health_document_composition(self):
+ id = str(uuid()) # TODO: use identifiable id
+ return Composition(
+ id=id,
+ identifier=Identifier(value=id),
+ status="final", # TODO: use appropriate one
+ type=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="419891008",
+ display="Record artifact",
+ )
+ ]
+ ),
+ title="Health Document Record",
+ date=datetime.now(timezone.utc).isoformat(),
+ section=[
+ CompositionSection(
+ title="Health Document Record",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="419891008",
+ display="Record artifact",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda file: self._reference(
+ self._document_reference(file)
+ ),
+ FileUpload.objects.filter(
+ associating_id=self.consultation.id
+ ),
+ )
+ ),
+ )
+ ],
+ subject=self._reference(self._patient()),
+ encounter=self._reference(self._encounter()),
+ author=[self._reference(self._organization())],
+ )
+
+ def _wellness_composition(self):
+ id = str(uuid()) # TODO: use identifiable id
+ return Composition(
+ id=id,
+ identifier=Identifier(value=id),
+ status="final", # TODO: use appropriate one
+ type=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ display="Wellness Record",
+ )
+ ]
+ ),
+ title="Wellness Record",
+ date=datetime.now(timezone.utc).isoformat(),
+ section=list(
+ map(
+ lambda daily_round: CompositionSection(
+ title=f"Daily Round - {daily_round.created_date}",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ display="Wellness Record",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda observation_profile: self._reference(
+ observation_profile
+ ),
+ self._observations_from_daily_round(daily_round),
+ )
+ ),
+ ),
+ self.consultation.daily_rounds.all(),
+ )
+ ),
+ subject=self._reference(self._patient()),
+ encounter=self._reference(self._encounter()),
+ author=[self._reference(self._organization())],
+ )
+
+ def _immunization_composition(self):
+ id = str(uuid()) # TODO: use identifiable id
+ return Composition(
+ id=id,
+ identifier=Identifier(value=id),
+ status="final", # TODO: use appropriate one
+ type=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="41000179103",
+ display="Immunization Record",
+ ),
+ ],
+ ),
+ title="Immunization",
+ date=datetime.now(timezone.utc).isoformat(),
+ section=[
+ CompositionSection(
+ title="IPD Immunization",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="41000179103",
+ display="Immunization Record",
+ ),
+ ],
+ ),
+ entry=[
+ *(
+ [self._reference(self._immunization())]
+ if self._immunization()
+ else []
+ )
+ ],
+ emptyReason=None
+ if self._immunization()
+ else CodeableConcept(
+ coding=[Coding(code="notasked", display="Not Asked")]
+ ),
+ ),
+ ],
+ subject=self._reference(self._patient()),
+ encounter=self._reference(self._encounter()),
+ author=[self._reference(self._organization())],
+ )
+
+ def _diagnostic_report_composition(self):
+ id = str(uuid()) # TODO: use identifiable id
+ return Composition(
+ id=id,
+ identifier=Identifier(value=id),
+ status="final", # TODO: use appropriate one
+ type=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="721981007",
+ display="Diagnostic Report",
+ ),
+ ],
+ ),
+ title="Diagnostic Report",
+ date=datetime.now(timezone.utc).isoformat(),
+ section=[
+ CompositionSection(
+ title="Investigation Report",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="721981007",
+ display="Diagnostic Report",
+ ),
+ ],
+ ),
+ entry=[self._reference(self._diagnostic_report())],
+ ),
+ ],
+ subject=self._reference(self._patient()),
+ encounter=self._reference(self._encounter()),
+ author=[self._reference(self._organization())],
+ )
+
+ def _discharge_summary_composition(self):
+ id = str(uuid()) # TODO: use identifiable id
+ return Composition(
+ id=id,
+ identifier=Identifier(value=id),
+ status="final", # TODO: use appropriate one
+ type=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="373942005",
+ display="Discharge Summary Record",
+ )
+ ]
+ ),
+ title="Discharge Summary Document",
+ date=datetime.now(timezone.utc).isoformat(),
+ section=[
+ CompositionSection(
+ title="Prescribed medications",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="440545006",
+ display="Prescription",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda medicine: self._reference(
+ self._medication_request(medicine)[1]
+ ),
+ self.consultation.discharge_advice,
+ )
+ ),
+ ),
+ CompositionSection(
+ title="Health Documents",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="419891008",
+ display="Record",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda file: self._reference(
+ self._document_reference(file)
+ ),
+ FileUpload.objects.filter(
+ associating_id=self.consultation.id
+ ),
+ )
+ ),
+ ),
+ *list(
+ map(
+ lambda daily_round: CompositionSection(
+ title=f"Daily Round - {daily_round.created_date}",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ display="Wellness Record",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda observation_profile: self._reference(
+ observation_profile
+ ),
+ self._observations_from_daily_round(daily_round),
+ )
+ ),
+ ),
+ self.consultation.daily_rounds.all(),
+ )
+ ),
+ CompositionSection(
+ title="Procedures",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="371525003",
+ display="Clinical procedure report",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda procedure: self._reference(
+ self._procedure(procedure)
+ ),
+ self.consultation.procedure,
+ )
+ ),
+ ),
+ CompositionSection(
+ title="Care Plan",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="734163000",
+ display="Care Plan",
+ )
+ ]
+ ),
+ entry=[self._reference(self._careplan())],
+ ),
+ ],
+ subject=self._reference(self._patient()),
+ encounter=self._reference(self._encounter(include_diagnosis=True)),
+ author=[self._reference(self._organization())],
+ )
+
+ def _op_consultation_composition(self):
+ id = str(uuid()) # TODO: use identifiable id
+ return Composition(
+ id=id,
+ identifier=Identifier(value=id),
+ status="final", # TODO: use appropriate one
+ type=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="371530004",
+ display="Clinical consultation report",
+ )
+ ]
+ ),
+ title="OP Consultation Document",
+ date=datetime.now(timezone.utc).isoformat(),
+ section=[
+ CompositionSection(
+ title="Prescribed medications",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="440545006",
+ display="Prescription",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda medicine: self._reference(
+ self._medication_request(medicine)[1]
+ ),
+ self.consultation.discharge_advice,
+ )
+ ),
+ ),
+ CompositionSection(
+ title="Health Documents",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="419891008",
+ display="Record",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda file: self._reference(
+ self._document_reference(file)
+ ),
+ FileUpload.objects.filter(
+ associating_id=self.consultation.id
+ ),
+ )
+ ),
+ ),
+ *list(
+ map(
+ lambda daily_round: CompositionSection(
+ title=f"Daily Round - {daily_round.created_date}",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ display="Wellness Record",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda observation_profile: self._reference(
+ observation_profile
+ ),
+ self._observations_from_daily_round(daily_round),
+ )
+ ),
+ ),
+ self.consultation.daily_rounds.all(),
+ )
+ ),
+ CompositionSection(
+ title="Procedures",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="371525003",
+ display="Clinical procedure report",
+ )
+ ]
+ ),
+ entry=list(
+ map(
+ lambda procedure: self._reference(
+ self._procedure(procedure)
+ ),
+ self.consultation.procedure,
+ )
+ ),
+ ),
+ CompositionSection(
+ title="Care Plan",
+ code=CodeableConcept(
+ coding=[
+ Coding(
+ system="https://projecteka.in/sct",
+ code="734163000",
+ display="Care Plan",
+ )
+ ]
+ ),
+ entry=[self._reference(self._careplan())],
+ ),
+ ],
+ subject=self._reference(self._patient()),
+ encounter=self._reference(self._encounter(include_diagnosis=True)),
+ author=[self._reference(self._organization())],
+ )
+
+ def _bundle_entry(self, resource):
+ return BundleEntry(fullUrl=self._reference_url(resource), resource=resource)
+
+ def create_prescription_record(self):
+ id = str(uuid())
+ now = datetime.now(timezone.utc).isoformat()
+ return Bundle(
+ id=id,
+ identifier=Identifier(value=id),
+ type="document",
+ meta=Meta(lastUpdated=now),
+ timestamp=now,
+ entry=[
+ self._bundle_entry(self._prescription_composition()),
+ self._bundle_entry(self._practioner()),
+ self._bundle_entry(self._patient()),
+ self._bundle_entry(self._organization()),
+ self._bundle_entry(self._encounter()),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._medication_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._medication_request_profiles,
+ )
+ ),
+ ],
+ ).json()
+
+ def create_wellness_record(self):
+ id = str(uuid())
+ now = datetime.now(timezone.utc).isoformat()
+ return Bundle(
+ id=id,
+ identifier=Identifier(value=id),
+ type="document",
+ meta=Meta(lastUpdated=now),
+ timestamp=now,
+ entry=[
+ self._bundle_entry(self._wellness_composition()),
+ self._bundle_entry(self._practioner()),
+ self._bundle_entry(self._patient()),
+ self._bundle_entry(self._organization()),
+ self._bundle_entry(self._encounter()),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._observation_profiles,
+ )
+ ),
+ ],
+ ).json()
+
+ def create_immunization_record(self):
+ id = str(uuid())
+ now = datetime.now(timezone.utc).isoformat()
+ return Bundle(
+ id=id,
+ identifier=Identifier(value=id),
+ type="document",
+ meta=Meta(lastUpdated=now),
+ timestamp=now,
+ entry=[
+ self._bundle_entry(self._immunization_composition()),
+ self._bundle_entry(self._practioner()),
+ self._bundle_entry(self._patient()),
+ self._bundle_entry(self._organization()),
+ self._bundle_entry(self._encounter()),
+ self._bundle_entry(self._immunization()),
+ ],
+ ).json()
+
+ def create_diagnostic_report_record(self):
+ id = str(uuid())
+ now = datetime.now(timezone.utc).isoformat()
+ return Bundle(
+ id=id,
+ identifier=Identifier(value=id),
+ type="document",
+ meta=Meta(lastUpdated=now),
+ timestamp=now,
+ entry=[
+ self._bundle_entry(self._diagnostic_report_composition()),
+ self._bundle_entry(self._practioner()),
+ self._bundle_entry(self._patient()),
+ self._bundle_entry(self._organization()),
+ self._bundle_entry(self._encounter()),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._observation_profiles,
+ )
+ ),
+ ],
+ ).json()
+
+ def create_health_document_record(self):
+ id = str(uuid())
+ now = datetime.now(timezone.utc).isoformat()
+ return Bundle(
+ id=id,
+ identifier=Identifier(value=id),
+ type="document",
+ meta=Meta(lastUpdated=now),
+ timestamp=now,
+ entry=[
+ self._bundle_entry(self._health_document_composition()),
+ self._bundle_entry(self._practioner()),
+ self._bundle_entry(self._patient()),
+ self._bundle_entry(self._organization()),
+ self._bundle_entry(self._encounter()),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._document_reference_profiles,
+ )
+ ),
+ ],
+ ).json()
+
+ def create_discharge_summary_record(self):
+ id = str(uuid())
+ now = datetime.now(timezone.utc).isoformat()
+ return Bundle(
+ id=id,
+ identifier=Identifier(value=id),
+ type="document",
+ meta=Meta(lastUpdated=now),
+ timestamp=now,
+ entry=[
+ self._bundle_entry(self._discharge_summary_composition()),
+ self._bundle_entry(self._practioner()),
+ self._bundle_entry(self._patient()),
+ self._bundle_entry(self._organization()),
+ self._bundle_entry(self._encounter()),
+ self._bundle_entry(self._careplan()),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._medication_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._medication_request_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._condition_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._procedure_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._document_reference_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._observation_profiles,
+ )
+ ),
+ ],
+ ).json()
+
+ def create_op_consultation_record(self):
+ id = str(uuid())
+ now = datetime.now(timezone.utc).isoformat()
+ return Bundle(
+ id=id,
+ identifier=Identifier(value=id),
+ type="document",
+ meta=Meta(lastUpdated=now),
+ timestamp=now,
+ entry=[
+ self._bundle_entry(self._op_consultation_composition()),
+ self._bundle_entry(self._practioner()),
+ self._bundle_entry(self._patient()),
+ self._bundle_entry(self._organization()),
+ self._bundle_entry(self._encounter()),
+ self._bundle_entry(self._careplan()),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._medication_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._medication_request_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._condition_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._procedure_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._document_reference_profiles,
+ )
+ ),
+ *list(
+ map(
+ lambda resource: self._bundle_entry(resource),
+ self._observation_profiles,
+ )
+ ),
+ ],
+ ).json()
+
+ def create_record(self, record_type):
+ if record_type == "Prescription":
+ return self.create_prescription_record()
+ elif record_type == "WellnessRecord":
+ return self.create_wellness_record()
+ elif record_type == "ImmunizationRecord":
+ return self.create_immunization_record()
+ elif record_type == "HealthDocumentRecord":
+ return self.create_health_document_record()
+ elif record_type == "DiagnosticReport":
+ return self.create_diagnostic_report_record()
+ elif record_type == "DischargeSummary":
+ return self.create_discharge_summary_record()
+ elif record_type == "OPConsultation":
+ return self.create_op_consultation_record()
+ else:
+ return self.create_discharge_summary_record()
diff --git a/care/abdm/views.py b/care/abdm/views.py
new file mode 100644
index 0000000000..60f00ef0ef
--- /dev/null
+++ b/care/abdm/views.py
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/care/facility/api/serializers/asset.py b/care/facility/api/serializers/asset.py
index fbb9f44970..7e672c4a01 100644
--- a/care/facility/api/serializers/asset.py
+++ b/care/facility/api/serializers/asset.py
@@ -18,6 +18,7 @@
from care.facility.api.serializers.facility import FacilityBareMinimumSerializer
from care.facility.models.asset import (
Asset,
+ AssetAvailabilityRecord,
AssetLocation,
AssetTransaction,
UserDefaultAssetLocation,
@@ -165,6 +166,15 @@ class Meta:
exclude = ("deleted", "external_id")
+class AssetAvailabilitySerializer(ModelSerializer):
+ id = UUIDField(source="external_id", read_only=True)
+ asset = AssetBareMinimumSerializer(read_only=True)
+
+ class Meta:
+ model = AssetAvailabilityRecord
+ exclude = ("deleted", "external_id")
+
+
class UserDefaultAssetLocationSerializer(ModelSerializer):
location_object = AssetLocationSerializer(source="location", read_only=True)
diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py
index e1fe82f098..b0d60fc81b 100644
--- a/care/facility/api/serializers/patient.py
+++ b/care/facility/api/serializers/patient.py
@@ -6,6 +6,8 @@
from django.utils.timezone import localtime, make_aware, now
from rest_framework import serializers
+from care.abdm.api.serializers.abhanumber import AbhaNumberSerializer
+from care.abdm.models import AbhaNumber
from care.facility.api.serializers import TIMESTAMP_FIELDS
from care.facility.api.serializers.facility import (
FacilityBasicInfoSerializer,
@@ -209,6 +211,11 @@ class Meta:
allow_transfer = serializers.BooleanField(default=settings.PEACETIME_MODE)
+ abha_number = ExternalIdSerializerField(
+ queryset=AbhaNumber.objects.all(), required=False, allow_null=True
+ )
+ abha_number_object = AbhaNumberSerializer(source="abha_number", read_only=True)
+
class Meta:
model = PatientRegistration
exclude = (
@@ -455,6 +462,7 @@ def save(self, **kwargs):
class PatientNotesSerializer(serializers.ModelSerializer):
facility = FacilityBasicInfoSerializer(read_only=True)
created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True)
+ created_by_local_user = serializers.BooleanField(read_only=True)
def validate_empty_values(self, data):
if not data.get("note", "").strip():
@@ -463,5 +471,11 @@ def validate_empty_values(self, data):
class Meta:
model = PatientNotes
- fields = ("note", "facility", "created_by_object", "created_date")
+ fields = (
+ "note",
+ "facility",
+ "created_by_object",
+ "created_by_local_user",
+ "created_date",
+ )
read_only_fields = ("created_date",)
diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py
index 35977940e5..a282c7cb4c 100644
--- a/care/facility/api/serializers/patient_consultation.py
+++ b/care/facility/api/serializers/patient_consultation.py
@@ -5,6 +5,7 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
+from care.abdm.utils.api_call import AbdmGateway
from care.facility.api.serializers import TIMESTAMP_FIELDS
from care.facility.api.serializers.bed import ConsultationBedDetailSerializer
from care.facility.api.serializers.daily_round import DailyRoundSerializer
@@ -419,6 +420,7 @@ class Meta:
model = PatientConsultation
fields = (
"discharge_reason",
+ "referred_to_external",
"discharge_notes",
"discharge_date",
"discharge_prescription",
@@ -435,7 +437,10 @@ def validate(self, attrs):
raise ValidationError(
{"death_datetime": "This field value cannot be in the future."}
)
- if attrs.get("death_datetime") < self.instance.admission_date:
+ if (
+ self.instance.admission_date
+ and attrs.get("death_datetime") < self.instance.admission_date
+ ):
raise ValidationError(
{
"death_datetime": "This field value cannot be before the admission date."
@@ -452,7 +457,10 @@ def validate(self, attrs):
raise ValidationError(
{"discharge_date": "This field value cannot be in the future."}
)
- elif attrs.get("discharge_date") < self.instance.admission_date:
+ elif (
+ self.instance.admission_date
+ and attrs.get("discharge_date") < self.instance.admission_date
+ ):
raise ValidationError(
{
"discharge_date": "This field value cannot be before the admission date."
@@ -471,6 +479,18 @@ def save(self, **kwargs):
ConsultationBed.objects.filter(
consultation=self.instance, end_date__isnull=True
).update(end_date=now())
+ if patient.abha_number:
+ abha_number = patient.abha_number
+ AbdmGateway().fetch_modes(
+ {
+ "healthId": abha_number.abha_number,
+ "name": abha_number.name,
+ "gender": abha_number.gender,
+ "dateOfBirth": str(abha_number.date_of_birth),
+ "consultationId": abha_number.external_id,
+ "purpose": "LINK",
+ }
+ )
return instance
def create(self, validated_data):
diff --git a/care/facility/api/serializers/patient_otp.py b/care/facility/api/serializers/patient_otp.py
index 9d6951e883..7457ac8454 100644
--- a/care/facility/api/serializers/patient_otp.py
+++ b/care/facility/api/serializers/patient_otp.py
@@ -26,7 +26,7 @@ def send_sms(otp, phone_number):
sendSMS(
phone_number,
(
- f"CoronaSafe Network Patient Management System Login, OTP is {otp} . "
+ f"Open Healthcare Network Patient Management System Login, OTP is {otp} . "
"Please do not share this Confidential Login Token with anyone else"
),
)
diff --git a/care/facility/api/serializers/shifting.py b/care/facility/api/serializers/shifting.py
index 17bc352215..8be8fc8863 100644
--- a/care/facility/api/serializers/shifting.py
+++ b/care/facility/api/serializers/shifting.py
@@ -12,6 +12,7 @@
)
from care.facility.models import (
BREATHLESSNESS_CHOICES,
+ CATEGORY_CHOICES,
FACILITY_TYPES,
SHIFTING_STATUS_CHOICES,
VEHICLE_CHOICES,
@@ -217,6 +218,7 @@ class ShiftingSerializer(serializers.ModelSerializer):
last_edited_by_object = UserBaseMinimumSerializer(
source="last_edited_by", read_only=True
)
+ patient_category = ChoiceField(choices=CATEGORY_CHOICES, required=False)
ambulance_driver_name = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
@@ -327,8 +329,10 @@ def update(self, instance, validated_data):
new_instance = super().update(instance, validated_data)
patient = new_instance.patient
- patient.last_consultation.category = self.initial_data["patient_category"]
- patient.last_consultation.save()
+ patient_category = validated_data.pop("patient_category")
+ if patient.last_consultation and patient_category is not None:
+ patient.last_consultation.category = patient_category
+ patient.last_consultation.save(update_fields=["category"])
if (
"status" in validated_data
@@ -390,9 +394,10 @@ def create(self, validated_data):
patient.allow_transfer = True
patient.save()
- if patient.last_consultation:
- patient.last_consultation.category = self.initial_data["patient_category"]
- patient.last_consultation.save()
+ patient_category = validated_data.pop("patient_category")
+ if patient.last_consultation and patient_category is not None:
+ patient.last_consultation.category = patient_category
+ patient.last_consultation.save(update_fields=["category"])
validated_data["origin_facility"] = patient.facility
diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py
index 57ec16da51..2b5fc6e6b1 100644
--- a/care/facility/api/viewsets/asset.py
+++ b/care/facility/api/viewsets/asset.py
@@ -23,6 +23,7 @@
from rest_framework.viewsets import GenericViewSet
from care.facility.api.serializers.asset import (
+ AssetAvailabilitySerializer,
AssetLocationSerializer,
AssetSerializer,
AssetTransactionSerializer,
@@ -32,6 +33,7 @@
)
from care.facility.models.asset import (
Asset,
+ AssetAvailabilityRecord,
AssetLocation,
AssetTransaction,
UserDefaultAssetLocation,
@@ -128,6 +130,17 @@ def retrieve(self, request, *args, **kwargs):
return Response(hit)
+class AssetAvailabilityFilter(filters.FilterSet):
+ external_id = filters.CharFilter(field_name="asset__external_id")
+
+
+class AssetAvailabilityViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
+ queryset = AssetAvailabilityRecord.objects.all().select_related("asset")
+ serializer_class = AssetAvailabilitySerializer
+ filter_backends = (filters.DjangoFilterBackend,)
+ filterset_class = AssetAvailabilityFilter
+
+
class AssetViewSet(
ListModelMixin,
RetrieveModelMixin,
diff --git a/care/facility/api/viewsets/facility_users.py b/care/facility/api/viewsets/facility_users.py
index 8422991791..a03319ecf8 100644
--- a/care/facility/api/viewsets/facility_users.py
+++ b/care/facility/api/viewsets/facility_users.py
@@ -1,3 +1,4 @@
+from django.db.models import Prefetch
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import mixins
@@ -7,7 +8,7 @@
from care.facility.models.facility import Facility
from care.users.api.serializers.user import UserAssignedSerializer
-from care.users.models import User
+from care.users.models import Skill, User
class UserFilter(filters.FilterSet):
@@ -34,6 +35,12 @@ def get_queryset(self):
facility = Facility.objects.get(
external_id=self.kwargs.get("facility_external_id")
)
- return facility.users.filter(deleted=False).order_by("-last_login")
+ queryset = facility.users.filter(deleted=False).order_by("-last_login")
+ queryset = queryset.prefetch_related(
+ Prefetch(
+ "skills", queryset=Skill.objects.filter(userskill__deleted=False)
+ )
+ )
+ return queryset
except Facility.DoesNotExist:
raise ValidationError({"Facility": "Facility not found"})
diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py
index 6f458c2902..5e895eab62 100644
--- a/care/facility/api/viewsets/patient.py
+++ b/care/facility/api/viewsets/patient.py
@@ -5,7 +5,7 @@
from django.conf import settings
from django.contrib.postgres.search import TrigramSimilarity
from django.db import models
-from django.db.models import Case, When
+from django.db.models import BooleanField, Case, F, Value, When
from django.db.models.query_utils import Q
from django_filters import rest_framework as filters
from djqscsv import render_to_csv_response
@@ -595,6 +595,16 @@ class PatientNotesViewSet(
queryset = (
PatientNotes.objects.all()
.select_related("facility", "patient", "created_by")
+ .annotate(
+ created_by_local_user=Case(
+ When(
+ created_by__home_facility__external_id=F("facility__external_id"),
+ then=Value(True),
+ ),
+ default=Value(False),
+ output_field=BooleanField(),
+ )
+ )
.order_by("-created_date")
)
serializer_class = PatientNotesSerializer
@@ -617,6 +627,7 @@ def get_queryset(self):
q_filters |= Q(patient__last_consultation__assigned_to=user)
q_filters |= Q(patient__assigned_to=user)
queryset = queryset.filter(q_filters)
+
return queryset
def perform_create(self, serializer):
diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py
index dc18b65851..98b60ba393 100644
--- a/care/facility/api/viewsets/patient_consultation.py
+++ b/care/facility/api/viewsets/patient_consultation.py
@@ -1,3 +1,4 @@
+from django.db.models import Prefetch
from django.db.models.query_utils import Q
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema
@@ -24,7 +25,7 @@
email_discharge_summary,
generate_and_upload_discharge_summary_task,
)
-from care.users.models import User
+from care.users.models import Skill, User
from care.utils.cache.cache_allowed_facilities import get_accessible_facilities
@@ -69,6 +70,14 @@ def get_permissions(self):
return super().get_permissions()
def get_queryset(self):
+ if self.serializer_class == PatientConsultationSerializer:
+ self.queryset = self.queryset.prefetch_related(
+ "assigned_to",
+ Prefetch(
+ "assigned_to__skills",
+ queryset=Skill.objects.filter(userskill__deleted=False),
+ ),
+ )
if self.request.user.is_superuser:
return self.queryset
elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]:
diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py
index ff1dfaa3a1..e6007e72c3 100644
--- a/care/facility/api/viewsets/prescription.py
+++ b/care/facility/api/viewsets/prescription.py
@@ -1,5 +1,3 @@
-from re import IGNORECASE
-
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema
@@ -140,36 +138,30 @@ class MedibaseViewSet(ViewSet):
permission_classes = (IsAuthenticated,)
def serailize_data(self, objects):
- result = []
- for object in objects:
- if type(object) == tuple:
- object = object[0]
- result.append(
- {
- "id": object.external_id,
- "name": object.name,
- "type": object.type,
- "generic": object.generic,
- "company": object.company,
- "contents": object.contents,
- "cims_class": object.cims_class,
- "atc_classification": object.atc_classification,
- }
- )
- return result
+ return [
+ {
+ "id": x[0],
+ "name": x[1],
+ "type": x[2],
+ "generic": x[3],
+ "company": x[4],
+ "contents": x[5],
+ "cims_class": x[6],
+ "atc_classification": x[7],
+ }
+ for x in objects
+ ]
def sort(self, query, results):
exact_matches = []
partial_matches = []
- for result in results:
- if type(result) == tuple:
- result = result[0]
- words = result.searchable.lower().split()
+ for x in results:
+ words = f"{x[1]} {x[3]} {x[4]}".lower().split()
if query in words:
- exact_matches.append(result)
+ exact_matches.append(x)
else:
- partial_matches.append(result)
+ partial_matches.append(x)
return exact_matches + partial_matches
@@ -178,10 +170,8 @@ def list(self, request):
queryset = MedibaseMedicineTable
- if request.GET.get("query", False):
- query = request.GET.get("query").strip().lower()
- queryset = queryset.where(
- searchable=queryset.re_match(r".*" + query + r".*", IGNORECASE)
- )
+ if query := request.query_params.get("query"):
+ query = query.strip().lower()
+ queryset = [x for x in queryset if query in f"{x[1]} {x[3]} {x[4]}".lower()]
queryset = self.sort(query, queryset)
return Response(self.serailize_data(queryset[:15]))
diff --git a/care/facility/management/commands/load_meta_icd11_diagnosis.py b/care/facility/management/commands/load_meta_icd11_diagnosis.py
index 9a339df9bb..2895608657 100644
--- a/care/facility/management/commands/load_meta_icd11_diagnosis.py
+++ b/care/facility/management/commands/load_meta_icd11_diagnosis.py
@@ -13,10 +13,70 @@ class Command(BaseCommand):
help = "Loads ICD11 data to a table in to database."
+ data = []
+ roots_lookup = {}
+ """
+ Eg:
+ ```
+ {
+ "http://id.who.int/icd/entity/594985340": {
+ "chapter": "Certain infectious or parasitic diseases",
+ "block": "Intestinal infectious diseases",
+ "category": None,
+ },
+ }
+ ```
+ """
+
+ CLASS_KIND_DB_KEYS = {
+ "block": "root_block",
+ "category": "root_category",
+ }
+
+ def find_roots(self, item):
+ id = item["ID"]
+
+ if id in self.roots_lookup:
+ return self.roots_lookup[id]
+
+ if not item["parentId"]:
+ self.roots_lookup[id] = {item["classKind"]: item["label"]}
+ return self.roots_lookup[id]
+
+ if parent := self.roots_lookup.get(item["parentId"]):
+
+ def my(x):
+ return item["label"] if item["classKind"] == x else None
+
+ self.roots_lookup[id] = {
+ "chapter": parent.get("chapter") or my("chapter"),
+ "block": parent.get("block") or my("block"),
+ "category": parent.get("category") or my("category"),
+ }
+ return self.roots_lookup[id]
+
+ # The following code is never executed as the `icd11.json` file is
+ # pre-sorted and hence the parent is always present before the child.
+ print("Full-scan for", id, item["label"])
+ return self.find_roots(
+ [
+ icd11_object
+ for icd11_object in self.data
+ if icd11_object["ID"] == item["parentId"]
+ ][0]
+ )
+
def handle(self, *args, **options):
- print("Loading ICD11 data to database...")
+ print("Loading ICD11 data to DB Table (meta_icd11_diagnosis)...")
try:
- icd11_objects = fetch_data()
+ self.data = fetch_data()
+
+ def roots(item):
+ return {
+ self.CLASS_KIND_DB_KEYS.get(k, k): v
+ for k, v in self.find_roots(item).items()
+ }
+
MetaICD11Diagnosis.objects.all().delete()
MetaICD11Diagnosis.objects.bulk_create(
[
@@ -30,8 +90,9 @@ def handle(self, *args, **options):
is_leaf=icd11_object["isLeaf"],
label=icd11_object["label"],
breadth_value=icd11_object["breadthValue"],
+ **roots(icd11_object),
)
- for icd11_object in icd11_objects
+ for icd11_object in self.data
if icd11_object["ID"].split("/")[-1].isnumeric()
]
)
diff --git a/care/facility/migrations/0371_metaicd11diagnosis_chapter_and_more.py b/care/facility/migrations/0371_metaicd11diagnosis_chapter_and_more.py
new file mode 100644
index 0000000000..342c2014d3
--- /dev/null
+++ b/care/facility/migrations/0371_metaicd11diagnosis_chapter_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.2 on 2023-07-12 13:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("facility", "0370_merge_20230705_1500"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="metaicd11diagnosis",
+ name="chapter",
+ field=models.CharField(default="", max_length=255),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="metaicd11diagnosis",
+ name="root_block",
+ field=models.CharField(max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name="metaicd11diagnosis",
+ name="root_category",
+ field=models.CharField(max_length=255, null=True),
+ ),
+ ]
diff --git a/care/facility/migrations/0372_assetavailabilityrecord.py b/care/facility/migrations/0372_assetavailabilityrecord.py
new file mode 100644
index 0000000000..2d3dc2daf4
--- /dev/null
+++ b/care/facility/migrations/0372_assetavailabilityrecord.py
@@ -0,0 +1,66 @@
+# Generated by Django 4.2.2 on 2023-07-18 05:00
+
+import uuid
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("facility", "0371_metaicd11diagnosis_chapter_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="AssetAvailabilityRecord",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ 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)),
+ (
+ "status",
+ models.CharField(
+ choices=[
+ ("Not Monitored", "Not Monitored"),
+ ("Operational", "Operational"),
+ ("Down", "Down"),
+ ("Under Maintenance", "Under Maintenance"),
+ ],
+ default="Not Monitored",
+ max_length=20,
+ ),
+ ),
+ ("timestamp", models.DateTimeField()),
+ (
+ "asset",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT, to="facility.asset"
+ ),
+ ),
+ ],
+ options={
+ "ordering": ["-timestamp"],
+ "unique_together": {("asset", "timestamp")},
+ },
+ ),
+ ]
diff --git a/care/facility/migrations/0373_remove_patientconsultation_hba1c.py b/care/facility/migrations/0373_remove_patientconsultation_hba1c.py
new file mode 100644
index 0000000000..d4b200cce1
--- /dev/null
+++ b/care/facility/migrations/0373_remove_patientconsultation_hba1c.py
@@ -0,0 +1,16 @@
+# Generated by Django 4.2.2 on 2023-07-20 13:09
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("facility", "0372_assetavailabilityrecord"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="patientconsultation",
+ name="HBA1C",
+ ),
+ ]
diff --git a/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py
new file mode 100644
index 0000000000..304bebb163
--- /dev/null
+++ b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py
@@ -0,0 +1,44 @@
+# Generated by Django 4.2.2 on 2023-07-20 17:45
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"),
+ ("facility", "0373_remove_patientconsultation_hba1c"),
+ ]
+
+ replaces = [
+ ("facility", "0329_auto_20221219_1936"),
+ ("facility", "0330_auto_20221220_2312"),
+ ("facility", "0366_merge_20230628_1428"),
+ ("facility", "0366_merge_20230628_1428"),
+ ("facility", "0373_merge_20230719_1143"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="historicalpatientregistration",
+ name="abha_number",
+ field=models.ForeignKey(
+ blank=True,
+ db_constraint=False,
+ null=True,
+ on_delete=django.db.models.deletion.DO_NOTHING,
+ related_name="+",
+ to="abdm.abhanumber",
+ ),
+ ),
+ migrations.AddField(
+ model_name="patientregistration",
+ name="abha_number",
+ field=models.OneToOneField(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="abdm.abhanumber",
+ ),
+ ),
+ ]
diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py
index ac14fba3a0..fdeaff4263 100644
--- a/care/facility/models/asset.py
+++ b/care/facility/models/asset.py
@@ -17,6 +17,13 @@ def get_random_asset_id():
return str(uuid.uuid4())
+class AvailabilityStatus(models.TextChoices):
+ NOT_MONITORED = "Not Monitored"
+ OPERATIONAL = "Operational"
+ DOWN = "Down"
+ UNDER_MAINTENANCE = "Under Maintenance"
+
+
class AssetLocation(BaseModel, AssetsPermissionMixin):
"""
This model is also used to store rooms that the assets are in, Since these rooms are mapped to
@@ -105,6 +112,34 @@ def __str__(self):
return self.name
+class AssetAvailabilityRecord(BaseModel):
+ """
+ Model to store the availability status of an asset at a particular timestamp.
+
+ Fields:
+ - asset: ForeignKey to Asset model
+ - status: CharField with choices from AvailabilityStatus
+ - timestamp: DateTimeField to store the timestamp of the availability record
+
+ Note: A pair of asset and timestamp together should be unique, not just the timestamp alone.
+ """
+
+ asset = models.ForeignKey(Asset, on_delete=models.PROTECT, null=False, blank=False)
+ status = models.CharField(
+ choices=AvailabilityStatus.choices,
+ default=AvailabilityStatus.NOT_MONITORED,
+ max_length=20,
+ )
+ timestamp = models.DateTimeField(null=False, blank=False)
+
+ class Meta:
+ unique_together = (("asset", "timestamp"),)
+ ordering = ["-timestamp"]
+
+ def __str__(self):
+ return f"{self.asset.name} - {self.status} - {self.timestamp}"
+
+
class UserDefaultAssetLocation(BaseModel):
user = models.ForeignKey(User, on_delete=models.PROTECT, null=False, blank=False)
location = models.ForeignKey(
diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py
index d8873e3d28..d42958220b 100644
--- a/care/facility/models/file_upload.py
+++ b/care/facility/models/file_upload.py
@@ -125,3 +125,14 @@ def get_object(self, bucket=settings.FILE_UPLOAD_BUCKET, **kwargs):
Key=f"{self.FileType(self.file_type).name}/{self.internal_name}",
**kwargs,
)
+
+ def file_contents(self):
+ s3Client = boto3.client("s3", **cs_provider.get_client_config())
+ response = s3Client.get_object(
+ Bucket=settings.FILE_UPLOAD_BUCKET,
+ Key=self.FileType(self.file_type).name + "/" + self.internal_name,
+ )
+
+ content_type = response["ContentType"]
+ content = response["Body"].read()
+ return content_type, content
diff --git a/care/facility/models/meta_icd11_diagnosis.py b/care/facility/models/meta_icd11_diagnosis.py
index d59cc4ebfd..a3ea15f5b5 100644
--- a/care/facility/models/meta_icd11_diagnosis.py
+++ b/care/facility/models/meta_icd11_diagnosis.py
@@ -2,6 +2,10 @@
class MetaICD11Diagnosis(models.Model):
+ """
+ Not for production use. For Metabase purposes only. Do not build relations to this model.
+ """
+
id = models.CharField(max_length=255, primary_key=True)
_id = models.IntegerField()
average_depth = models.IntegerField()
@@ -11,6 +15,9 @@ class MetaICD11Diagnosis(models.Model):
is_leaf = models.BooleanField()
label = models.CharField(max_length=255)
breadth_value = models.DecimalField(max_digits=24, decimal_places=22)
+ chapter = models.CharField(max_length=255)
+ root_block = models.CharField(max_length=255, null=True)
+ root_category = models.CharField(max_length=255, null=True)
class Meta:
db_table = "meta_icd11_diagnosis"
diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py
index 36fa2c9c62..4131fef636 100644
--- a/care/facility/models/patient.py
+++ b/care/facility/models/patient.py
@@ -6,6 +6,7 @@
from django.db.models import JSONField
from simple_history.models import HistoricalRecords
+from care.abdm.models import AbhaNumber
from care.facility.models import (
DISEASE_CHOICES,
DiseaseStatusEnum,
@@ -410,6 +411,11 @@ class TestTypeEnum(enum.Enum):
related_name="root_patient_assigned_to",
)
+ # ABDM Health ID
+ abha_number = models.OneToOneField(
+ AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True
+ )
+
history = HistoricalRecords(excluded_fields=["meta_info"])
objects = BaseManager()
diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py
index de097a16ce..281401aa26 100644
--- a/care/facility/models/patient_consultation.py
+++ b/care/facility/models/patient_consultation.py
@@ -173,12 +173,6 @@ class PatientConsultation(PatientBaseModel, PatientRelatedPermissionMixin):
verbose_name="Patient's Weight in KG",
validators=[MinValueValidator(0)],
)
- HBA1C = models.FloatField(
- default=None,
- null=True,
- verbose_name="HBA1C parameter for reference to current blood sugar levels",
- validators=[MinValueValidator(0)],
- )
# ICU Information
diff --git a/care/facility/models/patient_icmr.py b/care/facility/models/patient_icmr.py
index 660c6cfcfd..e2f0ac9752 100644
--- a/care/facility/models/patient_icmr.py
+++ b/care/facility/models/patient_icmr.py
@@ -225,7 +225,7 @@ def is_symptomatic(self):
def symptomatic_international_traveller(
self,
):
- return (
+ return bool(
self.patient.countries_travelled
and len(self.patient.countries_travelled) != 0
and (
diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py
index 93221302ad..b6aca21eab 100644
--- a/care/facility/models/prescription.py
+++ b/care/facility/models/prescription.py
@@ -64,7 +64,7 @@ class MedibaseMedicine(BaseModel):
atc_classification = models.TextField(blank=True, null=True)
def __str__(self):
- return " - ".join([self.name, self.generic, self.company])
+ return " - ".join(filter(None, [self.name, self.generic, self.company]))
class Prescription(BaseModel):
diff --git a/care/facility/static_data/medibase.py b/care/facility/static_data/medibase.py
index 946360635f..fb5718c3df 100644
--- a/care/facility/static_data/medibase.py
+++ b/care/facility/static_data/medibase.py
@@ -1,26 +1,34 @@
-from littletable import Table
+from django.db.models import CharField, TextField, Value
+from django.db.models.functions import Coalesce
from care.facility.models.prescription import MedibaseMedicine
-MedibaseMedicineTable = Table("MedibaseMedicine")
-medibase_objects = MedibaseMedicine.objects.all()
-
-for obj in medibase_objects:
- MedibaseMedicineTable.insert(
- {
- "id": obj.id,
- "external_id": obj.external_id,
- "name": obj.name,
- "type": obj.type,
- "generic": obj.generic or "",
- "company": obj.company or "",
- "contents": obj.contents or "",
- "cims_class": obj.cims_class or "",
- "atc_classification": obj.atc_classification or "",
- "searchable": f"{obj.name} {obj.generic} {obj.company}",
- }
+def load_medibase_in_memory():
+ return (
+ MedibaseMedicine.objects.all()
+ .annotate(
+ generic_pretty=Coalesce("generic", Value(""), output_field=CharField()),
+ company_pretty=Coalesce("company", Value(""), output_field=CharField()),
+ contents_pretty=Coalesce("contents", Value(""), output_field=TextField()),
+ cims_class_pretty=Coalesce(
+ "cims_class", Value(""), output_field=CharField()
+ ),
+ atc_classification_pretty=Coalesce(
+ "atc_classification", Value(""), output_field=TextField()
+ ),
+ )
+ .values_list(
+ "external_id",
+ "name",
+ "type",
+ "generic_pretty",
+ "company_pretty",
+ "contents_pretty",
+ "cims_class_pretty",
+ "atc_classification_pretty",
+ )
)
-MedibaseMedicineTable.create_index("id", unique=True)
-MedibaseMedicineTable.create_search_index("searchable")
+
+MedibaseMedicineTable = load_medibase_in_memory()
diff --git a/care/facility/tasks/__init__.py b/care/facility/tasks/__init__.py
index 7ebf63cdaa..1a9383d32d 100644
--- a/care/facility/tasks/__init__.py
+++ b/care/facility/tasks/__init__.py
@@ -1,6 +1,7 @@
from celery import current_app
from celery.schedules import crontab
+from care.facility.tasks.asset_monitor import check_asset_status
from care.facility.tasks.cleanup import delete_old_notifications
from care.facility.tasks.summarisation import (
summarise_district_patient,
@@ -19,12 +20,12 @@ def setup_periodic_tasks(sender, **kwargs):
name="delete_old_notifications",
)
sender.add_periodic_task(
- crontab(hour="*/4", minute=59),
+ crontab(hour="*/4", minute="59"),
summarise_triage.s(),
name="summarise_triage",
)
sender.add_periodic_task(
- crontab(hour=23, minute=59),
+ crontab(hour="23", minute="59"),
summarise_tests.s(),
name="summarise_tests",
)
@@ -34,12 +35,17 @@ def setup_periodic_tasks(sender, **kwargs):
name="summarise_facility_capacity",
)
sender.add_periodic_task(
- crontab(hour="*/1", minute=59),
+ crontab(hour="*/1", minute="59"),
summarise_patient.s(),
name="summarise_patient",
)
sender.add_periodic_task(
- crontab(hour="*/1", minute=59),
+ crontab(hour="*/1", minute="59"),
summarise_district_patient.s(),
name="summarise_district_patient",
)
+ sender.add_periodic_task(
+ crontab(minute="*/30"),
+ check_asset_status.s(),
+ name="check_asset_status",
+ )
diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py
new file mode 100644
index 0000000000..2a30571b05
--- /dev/null
+++ b/care/facility/tasks/asset_monitor.py
@@ -0,0 +1,101 @@
+import logging
+from datetime import datetime
+from typing import Any
+
+from celery import shared_task
+from django.utils import timezone
+
+from care.facility.models.asset import (
+ Asset,
+ AssetAvailabilityRecord,
+ AvailabilityStatus,
+)
+from care.utils.assetintegration.asset_classes import AssetClasses
+from care.utils.assetintegration.base import BaseAssetIntegration
+
+logger = logging.getLogger(__name__)
+
+
+@shared_task
+def check_asset_status():
+ logger.info(f"Checking Asset Status: {timezone.now()}")
+
+ assets = Asset.objects.all()
+ middleware_status_cache = {}
+
+ for asset in assets:
+ # Skipping if asset class or local IP address is not present
+ if not asset.asset_class or not asset.meta.get("local_ip_address", None):
+ continue
+ try:
+ # Fetching middleware hostname
+ hostname = asset.meta.get(
+ "middleware_hostname",
+ asset.current_location.facility.middleware_address,
+ )
+ result: Any = None
+
+ # Checking if middleware status is already cached
+ if hostname in middleware_status_cache:
+ result = middleware_status_cache[hostname]
+ else:
+ try:
+ # Creating an instance of the asset class
+ asset_class: BaseAssetIntegration = AssetClasses[
+ asset.asset_class
+ ].value(
+ {
+ **asset.meta,
+ "middleware_hostname": hostname,
+ }
+ )
+ # Fetching the status of the device
+ result = asset_class.api_get(asset_class.get_url("devices/status"))
+ except Exception:
+ logger.warn(f"Middleware {hostname} is down", exc_info=True)
+
+ # If no status is returned, setting default status as down
+ if not result:
+ result = [{"time": timezone.now().isoformat(), "status": []}]
+
+ middleware_status_cache[hostname] = result
+
+ # Setting new status as down by default
+ new_status = AvailabilityStatus.DOWN
+ for status_record in result:
+ if asset.meta.get("local_ip_address") in status_record.get(
+ "status", {}
+ ):
+ asset_status = status_record["status"][
+ asset.meta.get("local_ip_address")
+ ]
+ else:
+ asset_status = "down"
+
+ # Fetching the last record of the asset
+ last_record = (
+ AssetAvailabilityRecord.objects.filter(asset=asset)
+ .order_by("-timestamp")
+ .first()
+ )
+
+ # Setting new status based on the status returned by the device
+ if asset_status == "up":
+ new_status = AvailabilityStatus.OPERATIONAL
+ elif asset_status == "maintenance":
+ new_status = AvailabilityStatus.UNDER_MAINTENANCE
+
+ # Creating a new record if the status has changed
+ if not last_record or (
+ datetime.fromisoformat(status_record.get("time"))
+ > last_record.timestamp
+ and last_record.status != new_status.value
+ ):
+ AssetAvailabilityRecord.objects.create(
+ asset=asset,
+ status=new_status.value,
+ timestamp=status_record.get("time", timezone.now()),
+ )
+
+ except Exception:
+ logger.error("Error in Asset Status Check", exc_info=True)
diff --git a/care/facility/templatetags/filters.py b/care/facility/templatetags/filters.py
index e3e4910300..9a8bc576fa 100644
--- a/care/facility/templatetags/filters.py
+++ b/care/facility/templatetags/filters.py
@@ -27,4 +27,7 @@ def field_name_to_label(value):
@register.filter(expects_localtime=True)
def parse_datetime(value):
- return datetime.strptime(value, "%Y-%m-%dT%H:%M")
+ try:
+ return datetime.strptime(value, "%Y-%m-%dT%H:%M")
+ except ValueError:
+ return None
diff --git a/care/facility/tests/test_asset_api.py b/care/facility/tests/test_asset_api.py
index dbb8c27d42..36cb493e49 100644
--- a/care/facility/tests/test_asset_api.py
+++ b/care/facility/tests/test_asset_api.py
@@ -11,17 +11,19 @@
class AssetViewSetTestCase(TestBase, TestClassMixin, APITestCase):
asset_id = None
- def setUp(self):
- self.factory = APIRequestFactory()
- state = self.create_state()
- district = self.create_district(state=state)
- self.user = self.create_user(district=district, username="test user")
- facility = self.create_facility(district=district, user=self.user)
- self.asset1_location = AssetLocation.objects.create(
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.factory = APIRequestFactory()
+ state = cls.create_state()
+ district = cls.create_district(state=state)
+ cls.user = cls.create_user(district=district, username="test user")
+ facility = cls.create_facility(district=district, user=cls.user)
+ cls.asset1_location = AssetLocation.objects.create(
name="asset1 location", location_type=1, facility=facility
)
- self.asset = Asset.objects.create(
- name="Test Asset", current_location=self.asset1_location, asset_type=50
+ cls.asset = Asset.objects.create(
+ name="Test Asset", current_location=cls.asset1_location, asset_type=50
)
def test_list_assets(self):
diff --git a/care/facility/tests/test_asset_availability_api.py b/care/facility/tests/test_asset_availability_api.py
new file mode 100644
index 0000000000..65b7d36e40
--- /dev/null
+++ b/care/facility/tests/test_asset_availability_api.py
@@ -0,0 +1,57 @@
+from django.utils import timezone
+from rest_framework import status
+from rest_framework.test import APIRequestFactory, APITestCase
+
+from care.facility.api.viewsets.asset import AssetAvailabilityViewSet
+from care.facility.models import Asset, AssetAvailabilityRecord, AssetLocation
+from care.facility.models.asset import AvailabilityStatus
+from care.facility.tests.mixins import TestClassMixin
+from care.utils.tests.test_base import TestBase
+
+
+class AssetAvailabilityViewSetTestCase(TestBase, TestClassMixin, APITestCase):
+ @classmethod
+ def setUp(cls):
+ cls.factory = APIRequestFactory()
+ state = cls.create_state()
+ district = cls.create_district(state=state)
+ cls.user = cls.create_user(district=district, username="test user")
+ facility = cls.create_facility(district=district, user=cls.user)
+ cls.asset_from_location = AssetLocation.objects.create(
+ name="asset from location", location_type=1, facility=facility
+ )
+ cls.asset_to_location = AssetLocation.objects.create(
+ name="asset to location", location_type=1, facility=facility
+ )
+ cls.asset = Asset.objects.create(
+ name="Test Asset", current_location=cls.asset_from_location, asset_type=50
+ )
+
+ cls.asset_availability = AssetAvailabilityRecord.objects.create(
+ asset=cls.asset,
+ status=AvailabilityStatus.OPERATIONAL.value,
+ timestamp=timezone.now(),
+ )
+
+ def test_list_asset_availability(self):
+ response = self.new_request(
+ ("/api/v1/asset_availability/",),
+ {"get": "list"},
+ AssetAvailabilityViewSet,
+ True,
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ response.data["results"][0]["status"], AvailabilityStatus.OPERATIONAL.value
+ )
+
+ def test_retrieve_asset_availability(self):
+ response = self.new_request(
+ (f"/api/v1/asset_availability/{self.asset_availability.id}/",),
+ {"get": "retrieve"},
+ AssetAvailabilityViewSet,
+ True,
+ {"pk": self.asset_availability.id},
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["status"], AvailabilityStatus.OPERATIONAL.value)
diff --git a/care/facility/tests/test_facilityuser_api.py b/care/facility/tests/test_facilityuser_api.py
new file mode 100644
index 0000000000..0e9d35edec
--- /dev/null
+++ b/care/facility/tests/test_facilityuser_api.py
@@ -0,0 +1,47 @@
+from django.test import TestCase
+from rest_framework import status
+
+from care.facility.api.viewsets.facility_users import FacilityUserViewSet
+from care.facility.models.facility import Facility
+from care.facility.tests.mixins import TestClassMixin
+from care.users.models import Skill
+
+
+class FacilityUserTest(TestClassMixin, TestCase):
+ def setUp(self):
+ super().setUp()
+ self.creator = self.users[0]
+
+ sample_data = {
+ "name": "Hospital X",
+ "ward": self.creator.ward,
+ "local_body": self.creator.local_body,
+ "district": self.creator.district,
+ "state": self.creator.state,
+ "facility_type": 1,
+ "address": "Nearby",
+ "pincode": 390024,
+ "features": [],
+ }
+ self.facility = Facility.objects.create(
+ external_id="550e8400-e29b-41d4-a716-446655440000",
+ created_by=self.creator,
+ **sample_data,
+ )
+
+ self.skill1 = Skill.objects.create(name="Skill 1")
+ self.skill2 = Skill.objects.create(name="Skill 2")
+
+ self.users[0].skills.add(self.skill1, self.skill2)
+
+ def test_get_queryset_with_prefetching(self):
+ response = self.new_request(
+ (f"/api/v1/facility/{self.facility.external_id}/get_users/",),
+ {"get": "list"},
+ FacilityUserViewSet,
+ self.users[0],
+ {"facility_external_id": self.facility.external_id},
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertNumQueries(2)
diff --git a/care/facility/tests/test_medibase_api.py b/care/facility/tests/test_medibase_api.py
index 34ce13d7d5..ec6b53afad 100644
--- a/care/facility/tests/test_medibase_api.py
+++ b/care/facility/tests/test_medibase_api.py
@@ -9,17 +9,17 @@ def get_url(self, query=None):
def test_search_by_name_exact_word(self):
response = self.client.get(self.get_url(query="dolo"))
- self.assertEquals(response.status_code, status.HTTP_200_OK)
- self.assertEquals(response.data[0]["name"], "DOLO")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data[0]["name"], "DOLO")
def test_search_by_generic_exact_word(self):
response = self.client.get(self.get_url(query="pAraCetAmoL"))
- self.assertEquals(response.status_code, status.HTTP_200_OK)
- self.assertEquals(response.data[0]["generic"], "paracetamol")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data[0]["generic"], "paracetamol")
def test_search_by_name_and_generic_exact_word(self):
response = self.client.get(self.get_url(query="panadol paracetamol"))
- self.assertEquals(response.status_code, status.HTTP_200_OK)
- self.assertEquals(response.data[0]["name"], "PANADOL")
- self.assertEquals(response.data[0]["generic"], "paracetamol")
- self.assertEquals(response.data[0]["company"], "GSK")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data[0]["name"], "PANADOL")
+ self.assertEqual(response.data[0]["generic"], "paracetamol")
+ self.assertEqual(response.data[0]["company"], "GSK")
diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py
index 86d96ade62..fb5b8bdd18 100644
--- a/care/facility/tests/test_patient_api.py
+++ b/care/facility/tests/test_patient_api.py
@@ -13,6 +13,7 @@ class ExpectedPatientNoteKeys(Enum):
FACILITY = "facility"
CREATED_BY_OBJECT = "created_by_object"
CREATED_DATE = "created_date"
+ CREATED_BY_LOCAL_USER = "created_by_local_user"
class ExpectedFacilityKeys(Enum):
@@ -71,7 +72,6 @@ class ExpectedCreatedByObjectKeys(Enum):
LAST_NAME = "last_name"
USER_TYPE = "user_type"
LAST_LOGIN = "last_login"
- HOME_FACILITY = "home_facility"
class PatientNotesTestCase(TestBase, TestClassMixin, APITestCase):
@@ -85,11 +85,23 @@ def setUp(self):
# Create users and facility
self.user = self.create_user(district=district, username="test user")
facility = self.create_facility(district=district, user=self.user)
+ self.user.home_facility = facility
+ self.user.save()
+
+ # Create another user from different facility
+ self.user2 = self.create_user(district=district, username="test user 2")
+ facility2 = self.create_facility(district=district, user=self.user2)
+ self.user2.home_facility = facility2
+ self.user2.save()
self.patient = self.create_patient(district=district.id)
self.patient_note = self.create_patient_note(
- patient=self.patient, facility=facility
+ patient=self.patient, facility=facility, created_by=self.user
+ )
+
+ self.patient_note2 = self.create_patient_note(
+ patient=self.patient, facility=facility, created_by=self.user2
)
refresh_token = RefreshToken.for_user(self.user)
@@ -103,14 +115,23 @@ def test_patient_notes(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIsInstance(response.json()["results"], list)
- # Ensure only necessary data is being sent and no extra data
+ # Test created_by_local_user field if user is not from same facility as patient
+ data2 = response.json()["results"][0]
- data = response.json()["results"][0]
+ created_by_local_user_content2 = data2["created_by_local_user"]
+ self.assertEqual(created_by_local_user_content2, False)
+
+ # Ensure only necessary data is being sent and no extra data
+ data = response.json()["results"][1]
self.assertCountEqual(
data.keys(), [item.value for item in ExpectedPatientNoteKeys]
)
+ created_by_local_user_content = data["created_by_local_user"]
+
+ self.assertEqual(created_by_local_user_content, True)
+
facility_content = data["facility"]
if facility_content is not None:
diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py
index 82e6a914ab..a3993f6c22 100644
--- a/care/facility/tests/test_patient_consultation_api.py
+++ b/care/facility/tests/test_patient_consultation_api.py
@@ -1,18 +1,62 @@
import datetime
+from django.test import TestCase
from django.utils.timezone import make_aware
from rest_framework import status
from rest_framework.test import APIRequestFactory, APITestCase
+from care.facility.api.viewsets.facility_users import FacilityUserViewSet
from care.facility.api.viewsets.patient_consultation import PatientConsultationViewSet
+from care.facility.models.facility import Facility
from care.facility.models.patient_consultation import (
CATEGORY_CHOICES,
PatientConsultation,
)
from care.facility.tests.mixins import TestClassMixin
+from care.users.models import Skill
from care.utils.tests.test_base import TestBase
+class FacilityUserTest(TestClassMixin, TestCase):
+ def setUp(self):
+ super().setUp()
+ self.creator = self.users[0]
+
+ sample_data = {
+ "name": "Hospital X",
+ "ward": self.creator.ward,
+ "local_body": self.creator.local_body,
+ "district": self.creator.district,
+ "state": self.creator.state,
+ "facility_type": 1,
+ "address": "Nearby",
+ "pincode": 390024,
+ "features": [],
+ }
+ self.facility = Facility.objects.create(
+ external_id="550e8400-e29b-41d4-a716-446655440000",
+ created_by=self.creator,
+ **sample_data,
+ )
+
+ self.skill1 = Skill.objects.create(name="Skill 1")
+ self.skill2 = Skill.objects.create(name="Skill 2")
+
+ self.users[0].skills.add(self.skill1, self.skill2)
+
+ def test_get_queryset_with_prefetching(self):
+ response = self.new_request(
+ (f"/api/v1/facility/{self.facility.external_id}/get_users/",),
+ {"get": "list"},
+ FacilityUserViewSet,
+ self.users[0],
+ {"facility_external_id": self.facility.external_id},
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertNumQueries(2)
+
+
class TestPatientConsultation(TestBase, TestClassMixin, APITestCase):
default_data = {
"symptoms": [1],
@@ -31,11 +75,7 @@ def setUp(self):
)
def create_admission_consultation(self, patient=None, **kwargs):
- patient = (
- self.create_patient(facility_id=self.facility.id)
- if not patient
- else patient
- )
+ patient = patient or self.create_patient(facility_id=self.facility.id)
data = self.default_data.copy()
kwargs.update(
{
@@ -148,3 +188,46 @@ def test_discharge_as_expired_after_admission(self):
discharge_date="2319-04-01T15:30:00Z",
)
self.assertEqual(res.status_code, status.HTTP_200_OK)
+
+ def test_referred_to_external_null(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ res = self.discharge(
+ consultation,
+ discharge_reason="REF",
+ discharge_date="2023-07-01T12:00:00Z",
+ discharge_notes="Discharged with null referred_to_external",
+ referred_to_external=None,
+ )
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+
+ def test_referred_to_external_empty_string(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ res = self.discharge(
+ consultation,
+ discharge_reason="REF",
+ discharge_date="2023-07-01T12:00:00Z",
+ discharge_notes="Discharged with empty referred_to_external",
+ referred_to_external="",
+ )
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+
+ def test_referred_to_external_valid_value(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ referred_to_external = "Test Hospital"
+ res = self.discharge(
+ consultation,
+ discharge_reason="REF",
+ discharge_date="2023-07-01T12:00:00Z",
+ discharge_notes="Discharged with valid referred_to_external",
+ referred_to_external=referred_to_external,
+ )
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py
index 4b675274c6..22733cfadb 100644
--- a/care/facility/tests/test_patient_daily_rounds_api.py
+++ b/care/facility/tests/test_patient_daily_rounds_api.py
@@ -10,4 +10,4 @@ def get_url(self, external_consultation_id=None):
def test_external_consultation_does_not_exists_returns_404(self):
sample_uuid = "e4a3d84a-d678-4992-9287-114f029046d8"
response = self.client.get(self.get_url(sample_uuid))
- self.assertEquals(response.status_code, status.HTTP_404_NOT_FOUND)
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
diff --git a/care/facility/utils/summarisation/facility_capacity.py b/care/facility/utils/summarisation/facility_capacity.py
index 607e4535c6..63548b9c3c 100644
--- a/care/facility/utils/summarisation/facility_capacity.py
+++ b/care/facility/utils/summarisation/facility_capacity.py
@@ -17,12 +17,7 @@
def facility_capacity_summary():
- capacity_objects = FacilityCapacity.objects.all().select_related(
- "facility",
- "facility__state",
- "facility__district",
- "facility__local_body",
- )
+ capacity_objects = FacilityCapacity.objects.all()
capacity_summary = {}
current_date = localtime(now()).replace(hour=0, minute=0, second=0, microsecond=0)
@@ -106,11 +101,10 @@ def facility_capacity_summary():
capacity_summary[facility_obj.id]["inventory"] = temp_inventory_summary_obj
for capacity_object in capacity_objects:
- facility_id = capacity_object.facility.id
+ facility_id = capacity_object.facility_id
if facility_id not in capacity_summary:
- capacity_summary[facility_id] = FacilitySerializer(
- capacity_object.facility
- ).data
+ # This facility is either deleted or not active
+ continue
if "availability" not in capacity_summary[facility_id]:
capacity_summary[facility_id]["availability"] = []
capacity_summary[facility_id]["availability"].append(
diff --git a/care/templates/base.html b/care/templates/base.html
index 9270456239..53b13042d1 100644
--- a/care/templates/base.html
+++ b/care/templates/base.html
@@ -104,7 +104,7 @@
alt="Digital Public Goods logo" />
- CoronaSafe Network is an open-source digital public good designed by
+ Open Healthcare Network is an open-source digital public good designed by
a multi-disciplinary team of innovators and volunteers who are working on a model to support
Government efforts.
(Github)
diff --git a/care/templates/email/user_reset_password.html b/care/templates/email/user_reset_password.html
index 7c5e29a087..8eb0100c40 100644
--- a/care/templates/email/user_reset_password.html
+++ b/care/templates/email/user_reset_password.html
@@ -1,5 +1,5 @@
Hi,
-Greetings from Coronasafe Network,
+Greetings from Open Healthcare Network,
Please click the following link to reset your password for your account with username {{username}}
Click Here
diff --git a/care/templates/email/user_reset_password.txt b/care/templates/email/user_reset_password.txt
index df328882a5..2a2e8a703f 100644
--- a/care/templates/email/user_reset_password.txt
+++ b/care/templates/email/user_reset_password.txt
@@ -1,5 +1,5 @@
Hi,
-Greetings from Coronasafe Network,
+Greetings from Open Healthcare Network,
Please click the following link to reset your password
{{reset_password_url}}
diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py
index c1fb589171..b845d3bdc4 100644
--- a/care/users/api/serializers/user.py
+++ b/care/users/api/serializers/user.py
@@ -344,7 +344,6 @@ class Meta:
"last_name",
"user_type",
"last_login",
- "home_facility",
)
@@ -353,11 +352,7 @@ class UserAssignedSerializer(serializers.ModelSerializer):
home_facility_object = FacilityBareMinimumSerializer(
source="home_facility", read_only=True
)
- skills = serializers.SerializerMethodField()
-
- def get_skills(self, obj):
- qs = obj.skills.filter(userskill__deleted=False)
- return SkillSerializer(qs, many=True).data
+ skills = SkillSerializer(many=True, read_only=True)
class Meta:
model = User
diff --git a/care/users/tests/test_facility_user_create.py b/care/users/tests/test_facility_user_create.py
index 75849c3180..66ae51d4f6 100644
--- a/care/users/tests/test_facility_user_create.py
+++ b/care/users/tests/test_facility_user_create.py
@@ -55,7 +55,7 @@ def test_create_facility_user__should_fail__when_higher_level(self):
response = self.client.post(self.get_url(), data=data, format="json")
# Test Creation
- self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_facility_user__should_fail__when_different_location(self):
new_district = self.clone_object(self.district)
@@ -64,4 +64,4 @@ def test_create_facility_user__should_fail__when_different_location(self):
response = self.client.post(self.get_url(), data=data, format="json")
# Test Creation
- self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py
index f703c277ac..92d318c3a5 100644
--- a/care/utils/assetintegration/base.py
+++ b/care/utils/assetintegration/base.py
@@ -46,9 +46,9 @@ def api_get(self, url, data=None):
headers={"Authorization": (self.auth_header_type + generate_jwt())},
)
try:
- response = req.json()
if req.status_code >= 400:
- raise APIException(response, req.status_code)
+ raise APIException(req.text, req.status_code)
+ response = req.json()
return response
except json.decoder.JSONDecodeError:
return {"error": "Invalid Response"}
diff --git a/config/api_router.py b/config/api_router.py
index 918ae39302..ff6235afb2 100644
--- a/config/api_router.py
+++ b/config/api_router.py
@@ -3,11 +3,14 @@
from rest_framework.routers import DefaultRouter, SimpleRouter
from rest_framework_nested.routers import NestedSimpleRouter
+from care.abdm.api.viewsets.abha import AbhaViewSet
+from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet
from care.facility.api.viewsets.ambulance import (
AmbulanceCreateViewSet,
AmbulanceViewSet,
)
from care.facility.api.viewsets.asset import (
+ AssetAvailabilityViewSet,
AssetLocationViewSet,
AssetPublicViewSet,
AssetTransactionViewSet,
@@ -185,11 +188,13 @@
router.register("asset", AssetViewSet)
router.register("asset_transaction", AssetTransactionViewSet)
+router.register("asset_availability", AssetAvailabilityViewSet)
patient_nested_router = NestedSimpleRouter(router, r"patient", lookup="patient")
patient_nested_router.register(r"test_sample", PatientSampleViewSet)
patient_nested_router.register(r"investigation", PatientInvestigationSummaryViewSet)
patient_nested_router.register(r"notes", PatientNotesViewSet)
+patient_nested_router.register(r"abha", AbhaViewSet)
consultation_nested_router = NestedSimpleRouter(
router, r"consultation", lookup="consultation"
@@ -211,6 +216,10 @@
# Public endpoints
router.register("public/asset", AssetPublicViewSet)
+# ABDM endpoints
+if settings.ENABLE_ABDM:
+ router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid")
+
app_name = "api"
urlpatterns = [
path("", include(router.urls)),
diff --git a/config/authentication.py b/config/authentication.py
index 0577dfa6b1..8d5a3e8281 100644
--- a/config/authentication.py
+++ b/config/authentication.py
@@ -2,6 +2,7 @@
import jwt
import requests
+from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from drf_spectacular.extensions import OpenApiAuthenticationExtension
@@ -156,6 +157,57 @@ def get_user(self, validated_token, facility):
return asset_user
+class ABDMAuthentication(JWTAuthentication):
+ def open_id_authenticate(self, url, token):
+ public_key = requests.get(url)
+ jwk = public_key.json()["keys"][0]
+ public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk))
+ return jwt.decode(
+ token, key=public_key, audience="account", algorithms=["RS256"]
+ )
+
+ def authenticate_header(self, request):
+ return "Bearer"
+
+ def authenticate(self, request):
+ jwt_token = request.META.get("HTTP_AUTHORIZATION")
+ if jwt_token is None:
+ return None
+ jwt_token = self.get_jwt_token(jwt_token)
+
+ abdm_cert_url = f"{settings.ABDM_URL}/gateway/v0.5/certs"
+ validated_token = self.get_validated_token(abdm_cert_url, jwt_token)
+
+ return self.get_user(validated_token), validated_token
+
+ def get_jwt_token(self, token):
+ return token.replace("Bearer", "").replace(" ", "")
+
+ def get_validated_token(self, url, token):
+ try:
+ return self.open_id_authenticate(url, token)
+ except Exception as e:
+ print(e)
+ raise InvalidToken({"detail": f"Invalid Authorization token: {e}"})
+
+ def get_user(self, validated_token):
+ user = User.objects.filter(username=settings.ABDM_USERNAME).first()
+ if not user:
+ password = User.objects.make_random_password()
+ user = User(
+ username=settings.ABDM_USERNAME,
+ email="hcx@coronasafe.network",
+ password=f"{password}123",
+ gender=3,
+ phone_number="917777777777",
+ user_type=User.TYPE_VALUE_MAP["Volunteer"],
+ verified=True,
+ age=10,
+ )
+ user.save()
+ return user
+
+
class CustomJWTAuthenticationScheme(OpenApiAuthenticationExtension):
target_class = "config.authentication.CustomJWTAuthentication"
name = "jwtAuth"
diff --git a/config/ratelimit.py b/config/ratelimit.py
index e1c7172795..9ba26a2704 100644
--- a/config/ratelimit.py
+++ b/config/ratelimit.py
@@ -25,6 +25,7 @@ def validatecaptcha(request):
return False
+# refer https://django-ratelimit.readthedocs.io/en/stable/rates.html for rate
def ratelimit(
request, group="", keys=[None], rate=settings.DJANGO_RATE_LIMIT, increment=True
):
diff --git a/config/settings/base.py b/config/settings/base.py
index 102e9a0af3..6e1fb60fae 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -112,6 +112,7 @@
]
LOCAL_APPS = [
"care.facility",
+ "care.abdm",
"care.users",
"care.audit_log",
"care.hcx",
@@ -539,11 +540,22 @@
json.loads(base64.b64decode(env("JWKS_BASE64", default=generate_encoded_jwks())))
)
+# ABDM
+ENABLE_ABDM = env.bool("ENABLE_ABDM", default=False)
+ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="")
+ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="")
+ABDM_URL = env("ABDM_URL", default="https://dev.abdm.gov.in")
+HEALTH_SERVICE_API_URL = env(
+ "HEALTH_SERVICE_API_URL", default="https://healthidsbx.abdm.gov.in/api"
+)
+ABDM_USERNAME = env("ABDM_USERNAME", default="abdm_user_internal")
+X_CM_ID = env("X_CM_ID", default="sbx")
+FIDELIUS_URL = env("FIDELIUS_URL", default="http://fidelius:8090")
+
IS_PRODUCTION = False
# HCX
-
HCX_PROTOCOL_BASE_PATH = env(
"HCX_PROTOCOL_BASE_PATH", default="http://staging-hcx.swasth.app/api/v0.7"
)
diff --git a/config/urls.py b/config/urls.py
index 9662e6383f..d2c22c5ce1 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -9,6 +9,7 @@
SpectacularSwaggerView,
)
+from care.abdm.urls import abdm_urlpatterns
from care.facility.api.viewsets.open_id import OpenIdConfigView
from care.hcx.api.viewsets.listener import (
ClaimOnSubmitView,
@@ -94,6 +95,9 @@
path("health/", include("healthy_django.urls", namespace="healthy_django")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+if settings.ENABLE_ABDM:
+ urlpatterns += abdm_urlpatterns
+
if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like.
diff --git a/config/wsgi.py b/config/wsgi.py
index 835d5c2ece..9827d177fd 100644
--- a/config/wsgi.py
+++ b/config/wsgi.py
@@ -36,3 +36,5 @@
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)
+
+from care.facility.static_data.medibase import MedibaseMedicineTable # noqa
diff --git a/data/dummy/facility.json b/data/dummy/facility.json
index e6b997828f..1564622b72 100644
--- a/data/dummy/facility.json
+++ b/data/dummy/facility.json
@@ -206,7 +206,6 @@
"current_bed": null,
"height": 0.0,
"weight": 0.0,
- "HBA1C": null,
"operation": null,
"special_instruction": "",
"intubation_history": []
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 61b59e95cf..495805aefb 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -38,5 +38,10 @@ services:
- "${TEMPDIR:-/tmp/localstack}:/tmp/localstack"
- "./docker/awslocal:/docker-entrypoint-initaws.d"
+ fidelius:
+ container_name: care_fidelius
+ image: khavinshankar/fidelius:v1.0
+ restart: always
+
volumes:
postgres-data:
diff --git a/docker/.local.env b/docker/.local.env
index 0de74cbf10..e755fb55b5 100644
--- a/docker/.local.env
+++ b/docker/.local.env
@@ -4,6 +4,7 @@ POSTGRES_HOST=db
POSTGRES_DB=care
POSTGRES_PORT=5432
DATABASE_URL=postgres://postgres:postgres@db:5432/care
+REDIS_URL=redis://redis:6379
CELERY_BROKER_URL=redis://redis:6379/0
DJANGO_DEBUG=False