From db7b2caf0c4936919b352d8afc53afad61f65fb0 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 12 Dec 2022 21:29:02 +0530 Subject: [PATCH 001/137] Basic Setup; Aadhaar Registration --- care/abdm/__init__.py | 0 care/abdm/admin.py | 1 + care/abdm/api/__init__.py | 0 care/abdm/api/serializers/auth.py | 13 ++ care/abdm/api/serializers/healthid.py | 124 ++++++++++++++++ care/abdm/api/viewsets/healthid.py | 126 ++++++++++++++++ care/abdm/apps.py | 13 ++ care/abdm/migrations/__init__.py | 0 care/abdm/models.py | 3 + care/abdm/tests.py | 3 + care/abdm/utils/api_call.py | 137 ++++++++++++++++++ care/abdm/views.py | 1 + .../migrations/0329_auto_20221212_1112.py | 35 +++++ care/facility/models/patient.py | 9 ++ care/utils/assetintegration/onvif.py | 11 +- config/api_router.py | 4 + config/settings/base.py | 4 + requirements/local.txt | 2 +- 18 files changed, 483 insertions(+), 3 deletions(-) create mode 100644 care/abdm/__init__.py create mode 100644 care/abdm/admin.py create mode 100644 care/abdm/api/__init__.py create mode 100644 care/abdm/api/serializers/auth.py create mode 100644 care/abdm/api/serializers/healthid.py create mode 100644 care/abdm/api/viewsets/healthid.py create mode 100644 care/abdm/apps.py create mode 100644 care/abdm/migrations/__init__.py create mode 100644 care/abdm/models.py create mode 100644 care/abdm/tests.py create mode 100644 care/abdm/utils/api_call.py create mode 100644 care/abdm/views.py create mode 100644 care/facility/migrations/0329_auto_20221212_1112.py 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/auth.py b/care/abdm/api/serializers/auth.py new file mode 100644 index 0000000000..6c4180aa8d --- /dev/null +++ b/care/abdm/api/serializers/auth.py @@ -0,0 +1,13 @@ +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() diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py new file mode 100644 index 0000000000..c66264e1bc --- /dev/null +++ b/care/abdm/api/serializers/healthid.py @@ -0,0 +1,124 @@ +from rest_framework.serializers import CharField, Serializer + + +class AadharOtpGenerateRequestPayloadSerializer(Serializer): + aadhaar = CharField( + max_length=16, + min_length=12, + required=True, + help_text="Aadhaar Number", + validators=[], + ) + + +class AadharOtpResendRequestPayloadSerializer(Serializer): + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Transaction ID", + validators=[], + ) + + +class GenerateMobileOtpRequestPayloadSerializer(Serializer): + mobile = CharField( + max_length=10, + min_length=10, + required=True, + help_text="Mobile Number", + validators=[], + ) + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Transaction ID", + validators=[], + ) + + +class VerifyOtpRequestPayloadSerializer(Serializer): + otp = CharField( + max_length=6, + min_length=6, + required=True, + help_text="OTP", + validators=[], + ) + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Transaction ID", + validators=[], + ) + + +# { +# "email": "Example@Demo.com", +# "firstName": "manoj", +# "healthId": "deepak.pant", +# "lastName": "singh", +# "middleName": "kishan", +# "password": "India@143", +# "profilePhoto": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkJCQkJCQoLCwoODw0PDhQSERESFB4WFxYXFh4uHSEdHSEdLikxKCUoMSlJOTMzOUlUR0NHVGZbW2aBeoGoqOIBCQkJCQkJCgsLCg4PDQ8OFBIRERIUHhYXFhcWHi4dIR0dIR0uKTEoJSgxKUk5MzM5SVRHQ0dUZltbZoF6gaio4v/CABEIBLAHgAMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAACAwABBAUGB//aAAgBAQAAAADwawLpMspcK7qrlE5F0Vtul2bVywMUNeBHUkW/bmxvYELGuNjh2VDvixxo5ViljKjDRMoahCULjs2JCShjhjh2OGxo0Y2MoXHOLszsKLhw7tD99mpZQxj8xceofmLEKFwXLTIyHwY1Ls+iEotjHY0M0pjRYxtGj4VFKLPohQlFQyy4Qipc0XG9pS+CP/2Q==", +# "txnId": "a825f76b-0696-40f3-864c-5a3a5b389a83" +# } +class CreateHealthIdSerializer(Serializer): + email = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Email", + validators=[], + ) + firstName = CharField( + max_length=64, + min_length=1, + required=False, + help_text="First Name", + validators=[], + ) + healthId = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Health ID", + validators=[], + ) + lastName = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Last Name", + validators=[], + ) + middleName = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Middle Name", + validators=[], + ) + password = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Password", + validators=[], + ) + profilePhoto = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Profile Photo", + validators=[], + ) + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="PreVerified Transaction ID", + validators=[], + ) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py new file mode 100644 index 0000000000..7ee04793f7 --- /dev/null +++ b/care/abdm/api/viewsets/healthid.py @@ -0,0 +1,126 @@ +# ABDM HealthID APIs + +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from care.abdm.api.serializers.healthid import ( + AadharOtpGenerateRequestPayloadSerializer, + AadharOtpResendRequestPayloadSerializer, + CreateHealthIdSerializer, + GenerateMobileOtpRequestPayloadSerializer, + VerifyOtpRequestPayloadSerializer, +) +from care.abdm.utils.api_call import HealthIdGateway + + +# API for Generating OTP for HealthID +class ABDMHealthIDViewSet(GenericViewSet): + base_name = "healthid" + + @swagger_auto_schema( + operation_id="generate_aadhaar_otp", + request_body=AadharOtpGenerateRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def generate_aadhaar_otp(self, request): + data = request.data + serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().generate_aadhaar_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/resendAadhaarOtp + operation_id="resend_aadhaar_otp", + request_body=AadharOtpResendRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def resend_aadhaar_otp(self, request): + data = request.data + serializer = AadharOtpResendRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().resend_aadhaar_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/verifyAadhaarOtp + operation_id="verify_aadhaar_otp", + request_body=VerifyOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def verify_aadhaar_otp(self, request): + data = request.data + serializer = VerifyOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().verify_aadhaar_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/generateMobileOTP + operation_id="generate_mobile_otp", + request_body=GenerateMobileOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def generate_mobile_otp(self, request): + data = request.data + serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().generate_mobile_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/verifyMobileOTP + operation_id="verify_mobile_otp", + request_body=VerifyOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def verify_mobile_otp(self, request): + data = request.data + serializer = VerifyOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().verify_mobile_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/createHealthId + operation_id="create_health_id", + request_body=CreateHealthIdSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def create_health_id(self, request): + data = request.data + serializer = CreateHealthIdSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().create_health_id(data) + return Response(response, status=status.HTTP_200_OK) + + # HealthID V2 APIs + @swagger_auto_schema( + # /v1/registration/aadhaar/checkAndGenerateMobileOTP + operation_id="check_and_generate_mobile_otp", + request_body=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 + 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/apps.py b/care/abdm/apps.py new file mode 100644 index 0000000000..f00da32cb9 --- /dev/null +++ b/care/abdm/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class AbdmConfig(AppConfig): + name = "abdm" + verbose_name = _("ABDM Integration") + + def ready(self): + try: + import care.abdm.signals # noqa F401 + except ImportError: + pass 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..0b4331b362 --- /dev/null +++ b/care/abdm/models.py @@ -0,0 +1,3 @@ +# from django.db import models + +# Create your models here. 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/utils/api_call.py b/care/abdm/utils/api_call.py new file mode 100644 index 0000000000..e23612f212 --- /dev/null +++ b/care/abdm/utils/api_call.py @@ -0,0 +1,137 @@ +import json + +import requests +from django.conf import settings +from django.core.cache import cache + +GATEWAY_API_URL = "https://dev.abdm.gov.in/" +HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" +ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" +ABDM_TOKEN_CACHE_KEY = "abdm_token" + + +class APIGateway: + def __init__(self, gateway, token): + if gateway == "health": + self.url = HEALTH_SERVICE_API_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_auth_header(self, headers): + token = cache.get(ABDM_TOKEN_CACHE_KEY) + print("Cached Token: {}".format(token)) + if not token: + 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 + ) + print("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": + print( + "Unsupported Content-Type: {}".format( + resp.headers["Content-Type"] + ) + ) + print("Response: {}".format(resp.text)) + return None + else: + data = resp.json() + token = data["accessToken"] + expires_in = data["expiresIn"] + print("New Token: {}".format(token)) + print("Expires in: {}".format(expires_in)) + cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) + else: + print("Bad Response: {}".format(resp.text)) + return None + print("Returning Authorization Header: Bearer {}".format(token)) + auth_header = {"Authorization": "Bearer {}".format(token)} + return {**headers, **auth_header} + + def get(self, path, params=None): + url = self.url + path + headers = {} + headers = self.add_auth_header(headers) + response = requests.get(url, headers=headers, params=params) + return response + + def post(self, path, data=None): + url = self.url + path + headers = { + "Content-Type": "application/json", + "accept": "*/*", + "Accept-Language": "en-US", + } + headers = self.add_auth_header(headers) + headers_string = " ".join( + ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] + ) + data_json = json.dumps(data) + print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) + response = requests.post(url, headers=headers, data=data_json) + 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) + print("{} 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 generate_mobile_otp(self, data): + path = "/v1/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" + response = self.api.post(path, data) + return response.json() + + +class HealthIdGatewayV2: + def __init__(self): + self.api = APIGateway("health", None) + + # V2 APIs + def check_and_generate_mobile_otp(self, data): + path = "/v2/registration/aadhaar/checkAndGenerateMobileOTP" + response = self.api.post(path, data) + return response.json() 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/migrations/0329_auto_20221212_1112.py b/care/facility/migrations/0329_auto_20221212_1112.py new file mode 100644 index 0000000000..fd68457684 --- /dev/null +++ b/care/facility/migrations/0329_auto_20221212_1112.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.11 on 2022-12-12 05:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0328_merge_20221208_1110'), + ] + + operations = [ + migrations.AddField( + model_name='historicalpatientregistration', + name='abha_number', + field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='ABHA Number'), + ), + migrations.AddField( + model_name='patientregistration', + name='abha_number', + field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='ABHA Number'), + ), + migrations.AlterField( + model_name='fileupload', + name='archived_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='archived_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='fileupload', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='uploaded_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 618cc5929e..a628e6abec 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -391,6 +391,15 @@ class TestTypeEnum(enum.Enum): related_name="root_patient_assigned_to", ) + # ABDM Health ID + abha_number = models.CharField( + max_length=255, + default=None, + verbose_name="ABHA Number", + null=True, + blank=True, + ) + history = HistoricalRecords(excluded_fields=["patient_search_id", "meta_info"]) objects = BaseManager() diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index f029e6c4dc..ecdae6d179 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -14,6 +14,8 @@ class OnvifActions(enum.Enum): GOTO_PRESET = "goto_preset" ABSOLUTE_MOVE = "absolute_move" RELATIVE_MOVE = "relative_move" + # STEP 1 | Part 1 + # GET_STREAMING_TOKEN = "getStreamingToken" def __init__(self, meta): try: @@ -23,7 +25,8 @@ def __init__(self, meta): self.access_key = self.meta["camera_access_key"].split(":")[2] except KeyError as e: raise ValidationError( - dict((key, f"{key} not found in asset metadata") for key in e.args)) + dict((key, f"{key} not found in asset metadata") for key in e.args) + ) def handle_action(self, action): action_type = action["type"] @@ -35,7 +38,7 @@ def handle_action(self, action): "username": self.username, "password": self.password, "accessKey": self.access_key, - **action_data + **action_data, } if action_type == self.OnvifActions.GET_CAMERA_STATUS.value: @@ -53,4 +56,8 @@ def handle_action(self, action): if action_type == self.OnvifActions.RELATIVE_MOVE.value: return self.api_post(self.get_url("relativeMove"), request_body) + # STEP 1 | Part 3 + # if action_type == self.OnvifActions.GET_STREAMING_TOKEN.value: + # return self.api_post(self.get_url("getStreamingToken"), request_body) + raise ValidationError({"action": "invalid action type"}) diff --git a/config/api_router.py b/config/api_router.py index dd949a3a1d..8fbc3bd85c 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -3,6 +3,7 @@ from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework_nested.routers import NestedSimpleRouter +from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet from care.facility.api.viewsets.ambulance import ( AmbulanceCreateViewSet, AmbulanceViewSet, @@ -193,6 +194,9 @@ # Public endpoints router.register("public/asset", AssetPublicViewSet) +# ABDM endpoints +router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") + app_name = "api" urlpatterns = [ url(r"^", include(router.urls)), diff --git a/config/settings/base.py b/config/settings/base.py index 31d0d5ccc6..6e0cbcc6e0 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -103,6 +103,7 @@ "care.users.apps.UsersConfig", "care.facility", "care.audit_log.apps.AuditLogConfig", + "care.abdm.apps.AbdmConfig", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -508,3 +509,6 @@ def GETKEY(group, request): JWKS = JsonWebKey.import_key_set( json.loads(base64.b64decode(env("JWKS_BASE64", default=generate_encoded_jwks()))) ) + +ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") +ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") diff --git a/requirements/local.txt b/requirements/local.txt index 84dcf6d7a7..cc56584e89 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -2,7 +2,7 @@ -r ./docs.txt Werkzeug==1.0.0 # https://github.com/pallets/werkzeug ipdb==0.13.2 # https://github.com/gotcha/ipdb -psycopg2-binary==2.8.4 # https://github.com/psycopg/psycopg2 +psycopg2-binary==2.8.6 # https://github.com/psycopg/psycopg2 # Code quality # ------------------------------------------------------------------------------ isort==5.10.1 # https://github.com/PyCQA/isort From 517adf46df04985a66e2dbe9979bc8ae3e59b9aa Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 19 Dec 2022 17:43:27 +0400 Subject: [PATCH 002/137] Add models --- care/abdm/api/viewsets/healthid.py | 8 +++++++- care/abdm/models.py | 22 ++++++++++++++++++++++ care/abdm/utils/api_call.py | 10 ++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 7ee04793f7..2b01d587b4 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -3,6 +3,7 @@ from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.decorators import action +from rest_framework.mixins import CreateModelMixin from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -13,12 +14,16 @@ GenerateMobileOtpRequestPayloadSerializer, VerifyOtpRequestPayloadSerializer, ) +from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import HealthIdGateway # API for Generating OTP for HealthID -class ABDMHealthIDViewSet(GenericViewSet): +class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): base_name = "healthid" + model = AbhaNumber + # Override Create method + # def create(self, request, *args, **kwargs): @swagger_auto_schema( operation_id="generate_aadhaar_otp", @@ -107,6 +112,7 @@ def create_health_id(self, request): serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().create_health_id(data) + print(response.status_code) return Response(response, status=status.HTTP_200_OK) # HealthID V2 APIs diff --git a/care/abdm/models.py b/care/abdm/models.py index 0b4331b362..6d5d3f07dd 100644 --- a/care/abdm/models.py +++ b/care/abdm/models.py @@ -1,3 +1,25 @@ # 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.CharField(max_length=50) + email = models.CharField(max_length=50) + first_name = models.CharField(max_length=50) + health_id = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + middle_name = models.CharField(max_length=50) + password = models.CharField(max_length=50) + profile_photo = models.CharField(max_length=50) + txn_id = models.CharField(max_length=50) + + access_token = models.CharField(max_length=50) + refresh_token = models.CharField(max_length=50) + + def __str__(self): + return self.abha_number diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index e23612f212..efc5f72f44 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -79,12 +79,14 @@ def post(self, path, data=None): "Accept-Language": "en-US", } headers = self.add_auth_header(headers) - headers_string = " ".join( - ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] - ) + # headers_string = " ".join( + # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] + # ) data_json = json.dumps(data) - print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) + # print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) + print("Posting Request to: {}".format(url)) response = requests.post(url, headers=headers, data=data_json) + print("{} Response: {}".format(response.status_code, response.text)) return response From ba2076330aeeea8afd46d6ce98120662993f994e Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Mon, 19 Dec 2022 19:55:56 +0530 Subject: [PATCH 003/137] Fix models issue --- .../migrations/0329_auto_20221212_1112.py | 35 ------------------- config/settings/base.py | 4 +-- 2 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 care/facility/migrations/0329_auto_20221212_1112.py diff --git a/care/facility/migrations/0329_auto_20221212_1112.py b/care/facility/migrations/0329_auto_20221212_1112.py deleted file mode 100644 index fd68457684..0000000000 --- a/care/facility/migrations/0329_auto_20221212_1112.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 2.2.11 on 2022-12-12 05:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('facility', '0328_merge_20221208_1110'), - ] - - operations = [ - migrations.AddField( - model_name='historicalpatientregistration', - name='abha_number', - field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='ABHA Number'), - ), - migrations.AddField( - model_name='patientregistration', - name='abha_number', - field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='ABHA Number'), - ), - migrations.AlterField( - model_name='fileupload', - name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='archived_by', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='fileupload', - name='uploaded_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='uploaded_by', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/config/settings/base.py b/config/settings/base.py index 6e0cbcc6e0..705ab9d694 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -103,7 +103,7 @@ "care.users.apps.UsersConfig", "care.facility", "care.audit_log.apps.AuditLogConfig", - "care.abdm.apps.AbdmConfig", + "care.abdm", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -269,7 +269,7 @@ "formatters": { "verbose": { "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" + "%(process)d %(thread)d %(message)s" } }, "handlers": { From c2e0a61e8ed928ba6a13c4cb2fd006aeb0e7fbf4 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Tue, 20 Dec 2022 23:37:33 +0530 Subject: [PATCH 004/137] Fixed patient to abha integration --- care/abdm/api/serializers/healthid.py | 44 +------------ care/abdm/api/viewsets/healthid.py | 32 +++++++++- care/abdm/migrations/0001_initial.py | 39 ++++++++++++ .../migrations/0002_auto_20221220_2312.py | 33 ++++++++++ .../migrations/0003_auto_20221220_2321.py | 22 +++++++ .../migrations/0004_auto_20221220_2325.py | 53 ++++++++++++++++ .../migrations/0005_auto_20221220_2327.py | 63 +++++++++++++++++++ care/abdm/models.py | 21 +++---- care/abdm/utils/api_call.py | 2 + .../migrations/0329_auto_20221219_1936.py | 25 ++++++++ .../migrations/0330_auto_20221220_2312.py | 25 ++++++++ care/facility/models/patient.py | 10 +-- 12 files changed, 306 insertions(+), 63 deletions(-) create mode 100644 care/abdm/migrations/0001_initial.py create mode 100644 care/abdm/migrations/0002_auto_20221220_2312.py create mode 100644 care/abdm/migrations/0003_auto_20221220_2321.py create mode 100644 care/abdm/migrations/0004_auto_20221220_2325.py create mode 100644 care/abdm/migrations/0005_auto_20221220_2327.py create mode 100644 care/facility/migrations/0329_auto_20221219_1936.py create mode 100644 care/facility/migrations/0330_auto_20221220_2312.py diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index c66264e1bc..0a164a5b1f 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -66,20 +66,6 @@ class VerifyOtpRequestPayloadSerializer(Serializer): # "txnId": "a825f76b-0696-40f3-864c-5a3a5b389a83" # } class CreateHealthIdSerializer(Serializer): - email = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Email", - validators=[], - ) - firstName = CharField( - max_length=64, - min_length=1, - required=False, - help_text="First Name", - validators=[], - ) healthId = CharField( max_length=64, min_length=1, @@ -87,34 +73,6 @@ class CreateHealthIdSerializer(Serializer): help_text="Health ID", validators=[], ) - lastName = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Last Name", - validators=[], - ) - middleName = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Middle Name", - validators=[], - ) - password = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Password", - validators=[], - ) - profilePhoto = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Profile Photo", - validators=[], - ) txnId = CharField( max_length=64, min_length=1, @@ -122,3 +80,5 @@ class CreateHealthIdSerializer(Serializer): help_text="PreVerified Transaction ID", validators=[], ) + patientId = CharField(required=True, help_text="Patient ID to be linked", + validators=[]) # TODO: Add UUID Validation diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 2b01d587b4..e6fe40b916 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -3,6 +3,7 @@ from drf_yasg.utils import swagger_auto_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.response import Response from rest_framework.viewsets import GenericViewSet @@ -16,15 +17,15 @@ ) from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import HealthIdGateway +from care.utils.queryset.patient import get_patient_queryset # API for Generating OTP for HealthID class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): base_name = "healthid" model = AbhaNumber - # Override Create method - # def create(self, request, *args, **kwargs): + # TODO: Ratelimiting for all endpoints that generate OTP's / Critical API's @swagger_auto_schema( operation_id="generate_aadhaar_otp", request_body=AadharOtpGenerateRequestPayloadSerializer, @@ -111,8 +112,33 @@ def create_health_id(self, request): data = request.data serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) + 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"}) response = HealthIdGateway().create_health_id(data) - print(response.status_code) + abha_object = AbhaNumber.objects.filter(abha_number=response["healthIdNumber"]).first() + if abha_object: + # Flow when abha number exists in db somehow! + pass + else: + # Create abha number flow + abha_object = AbhaNumber() + abha_object.abha_number = response["healthIdNumber"] + abha_object.email = response["email"] + abha_object.first_name = response["firstName"] + abha_object.health_id = response["healthId"] + abha_object.last_name = response["lastName"] + abha_object.middle_name = response["middleName"] + abha_object.profile_photo = response["profilePhoto"] + abha_object.txn_id = response["healthIdNumber"] + abha_object.access_token = response["token"] + abha_object.refresh_token = data["txnId"] + abha_object.save() + + patient_obj.abha_number = abha_object + patient_obj.save() return Response(response, status=status.HTTP_200_OK) # HealthID V2 APIs diff --git a/care/abdm/migrations/0001_initial.py b/care/abdm/migrations/0001_initial.py new file mode 100644 index 0000000000..20784886b7 --- /dev/null +++ b/care/abdm/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.11 on 2022-12-19 14:06 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AbhaNumber', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('abha_number', models.CharField(max_length=50)), + ('email', models.CharField(max_length=50)), + ('first_name', models.CharField(max_length=50)), + ('health_id', models.CharField(max_length=50)), + ('last_name', models.CharField(max_length=50)), + ('middle_name', models.CharField(max_length=50)), + ('password', models.CharField(max_length=50)), + ('profile_photo', models.CharField(max_length=50)), + ('txn_id', models.CharField(max_length=50)), + ('access_token', models.CharField(max_length=50)), + ('refresh_token', models.CharField(max_length=50)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/abdm/migrations/0002_auto_20221220_2312.py b/care/abdm/migrations/0002_auto_20221220_2312.py new file mode 100644 index 0000000000..fff590d3d5 --- /dev/null +++ b/care/abdm/migrations/0002_auto_20221220_2312.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='abhanumber', + name='email', + field=models.EmailField(max_length=254), + ), + migrations.AlterField( + model_name='abhanumber', + name='first_name', + field=models.CharField(max_length=512), + ), + migrations.AlterField( + model_name='abhanumber', + name='last_name', + field=models.CharField(max_length=512), + ), + migrations.AlterField( + model_name='abhanumber', + name='middle_name', + field=models.CharField(max_length=512), + ), + ] diff --git a/care/abdm/migrations/0003_auto_20221220_2321.py b/care/abdm/migrations/0003_auto_20221220_2321.py new file mode 100644 index 0000000000..7e2fd3121b --- /dev/null +++ b/care/abdm/migrations/0003_auto_20221220_2321.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0002_auto_20221220_2312'), + ] + + operations = [ + migrations.RemoveField( + model_name='abhanumber', + name='password', + ), + migrations.AlterField( + model_name='abhanumber', + name='profile_photo', + field=models.TextField(), + ), + ] diff --git a/care/abdm/migrations/0004_auto_20221220_2325.py b/care/abdm/migrations/0004_auto_20221220_2325.py new file mode 100644 index 0000000000..7e90626f80 --- /dev/null +++ b/care/abdm/migrations/0004_auto_20221220_2325.py @@ -0,0 +1,53 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0003_auto_20221220_2321'), + ] + + operations = [ + migrations.AlterField( + model_name='abhanumber', + name='abha_number', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='access_token', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='first_name', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='health_id', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='last_name', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='middle_name', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='refresh_token', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='txn_id', + field=models.TextField(), + ), + ] diff --git a/care/abdm/migrations/0005_auto_20221220_2327.py b/care/abdm/migrations/0005_auto_20221220_2327.py new file mode 100644 index 0000000000..f9781a1dbf --- /dev/null +++ b/care/abdm/migrations/0005_auto_20221220_2327.py @@ -0,0 +1,63 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0004_auto_20221220_2325'), + ] + + operations = [ + migrations.AlterField( + model_name='abhanumber', + name='abha_number', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='access_token', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='first_name', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='health_id', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='last_name', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='middle_name', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='profile_photo', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='refresh_token', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='txn_id', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/care/abdm/models.py b/care/abdm/models.py index 6d5d3f07dd..d931cc98c6 100644 --- a/care/abdm/models.py +++ b/care/abdm/models.py @@ -8,18 +8,17 @@ class AbhaNumber(BaseModel): - abha_number = models.CharField(max_length=50) - email = models.CharField(max_length=50) - first_name = models.CharField(max_length=50) - health_id = models.CharField(max_length=50) - last_name = models.CharField(max_length=50) - middle_name = models.CharField(max_length=50) - password = models.CharField(max_length=50) - profile_photo = models.CharField(max_length=50) - txn_id = models.CharField(max_length=50) + abha_number = models.TextField(null=True, blank=True) + email = models.EmailField(null=True, blank=True) + first_name = models.TextField(null=True, blank=True) + health_id = models.TextField(null=True, blank=True) + last_name = models.TextField(null=True, blank=True) + middle_name = models.TextField(null=True, blank=True) + profile_photo = models.TextField(null=True, blank=True) # What is profile photo? how is it stored as? + txn_id = models.TextField(null=True, blank=True) # 50? - access_token = models.CharField(max_length=50) - refresh_token = models.CharField(max_length=50) + access_token = models.TextField(null=True, blank=True) # 50 seems a bit too low for access tokens + refresh_token = models.TextField(null=True, blank=True) def __str__(self): return self.abha_number diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index efc5f72f44..a793650cfb 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -10,6 +10,8 @@ ABDM_TOKEN_CACHE_KEY = "abdm_token" +# TODO: Exception handling for all api calls, need to gracefully handle known exceptions + class APIGateway: def __init__(self, gateway, token): if gateway == "health": diff --git a/care/facility/migrations/0329_auto_20221219_1936.py b/care/facility/migrations/0329_auto_20221219_1936.py new file mode 100644 index 0000000000..7468cd13ec --- /dev/null +++ b/care/facility/migrations/0329_auto_20221219_1936.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.11 on 2022-12-19 14:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0328_merge_20221208_1110'), + ] + + operations = [ + migrations.AlterField( + model_name='fileupload', + name='archived_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='archived_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='fileupload', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='uploaded_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/care/facility/migrations/0330_auto_20221220_2312.py b/care/facility/migrations/0330_auto_20221220_2312.py new file mode 100644 index 0000000000..0d972fe276 --- /dev/null +++ b/care/facility/migrations/0330_auto_20221220_2312.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0002_auto_20221220_2312'), + ('facility', '0329_auto_20221219_1936'), + ] + + 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/patient.py b/care/facility/models/patient.py index a628e6abec..6997027460 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -7,6 +7,7 @@ from partial_index import PQ, PartialIndex from simple_history.models import HistoricalRecords +from care.abdm.models import AbhaNumber from care.facility.models import ( DISEASE_CHOICES, DiseaseStatusEnum, @@ -392,13 +393,8 @@ class TestTypeEnum(enum.Enum): ) # ABDM Health ID - abha_number = models.CharField( - max_length=255, - default=None, - verbose_name="ABHA Number", - null=True, - blank=True, - ) + + abha_number = models.OneToOneField(AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True) history = HistoricalRecords(excluded_fields=["patient_search_id", "meta_info"]) From d7cd31b221d3126c67e8e832b53a5cf7425170d0 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Tue, 20 Dec 2022 23:37:44 +0530 Subject: [PATCH 005/137] Fixed patient to abha integration --- care/abdm/migrations/0001_initial.py | 4 ++-- care/facility/migrations/0329_auto_20221219_1936.py | 9 +++++---- care/facility/migrations/0330_auto_20221220_2312.py | 10 ++++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/care/abdm/migrations/0001_initial.py b/care/abdm/migrations/0001_initial.py index 20784886b7..cb8481db18 100644 --- a/care/abdm/migrations/0001_initial.py +++ b/care/abdm/migrations/0001_initial.py @@ -1,11 +1,11 @@ # Generated by Django 2.2.11 on 2022-12-19 14:06 -from django.db import migrations, models import uuid +from django.db import migrations, models + class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/care/facility/migrations/0329_auto_20221219_1936.py b/care/facility/migrations/0329_auto_20221219_1936.py index 7468cd13ec..c0b161de69 100644 --- a/care/facility/migrations/0329_auto_20221219_1936.py +++ b/care/facility/migrations/0329_auto_20221219_1936.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.11 on 2022-12-19 14:06 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ ('facility', '0328_merge_20221208_1110'), ] @@ -15,11 +14,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='fileupload', name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='archived_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name='archived_by', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( model_name='fileupload', name='uploaded_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='uploaded_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name='uploaded_by', to=settings.AUTH_USER_MODEL), ), ] diff --git a/care/facility/migrations/0330_auto_20221220_2312.py b/care/facility/migrations/0330_auto_20221220_2312.py index 0d972fe276..53e3038ed7 100644 --- a/care/facility/migrations/0330_auto_20221220_2312.py +++ b/care/facility/migrations/0330_auto_20221220_2312.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.11 on 2022-12-20 17:42 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('abdm', '0002_auto_20221220_2312'), ('facility', '0329_auto_20221219_1936'), @@ -15,11 +14,14 @@ class Migration(migrations.Migration): 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'), + 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'), + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, + to='abdm.AbhaNumber'), ), ] From 51916373ef7f7b176103500b8a41bf56f8ba29f7 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Sun, 1 Jan 2023 20:44:24 +0530 Subject: [PATCH 006/137] Add V2 API's --- care/abdm/api/viewsets/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index e6fe40b916..63577d83bb 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -16,7 +16,7 @@ VerifyOtpRequestPayloadSerializer, ) from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import HealthIdGateway +from care.abdm.utils.api_call import HealthIdGateway, HealthIdGatewayV2 from care.utils.queryset.patient import get_patient_queryset @@ -37,7 +37,7 @@ def generate_aadhaar_otp(self, request): data = request.data serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGateway().generate_aadhaar_otp(data) + response = HealthIdGatewayV2().generate_aadhaar_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( From 354d58ce823a4e65c0d6cef84b4960228b26a8d2 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Sun, 1 Jan 2023 20:44:34 +0530 Subject: [PATCH 007/137] Add V2 API's --- care/abdm/utils/api_call.py | 43 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index a793650cfb..b5cccd5a39 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -9,9 +9,21 @@ ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" ABDM_TOKEN_CACHE_KEY = "abdm_token" - # TODO: Exception handling for all api calls, need to gracefully handle known exceptions +from Crypto.Cipher import PKCS1_OAEP +from Crypto.PublicKey import RSA +from base64 import b64encode + + +def encrypt_with_public_key(a_message): + rsa_public_key = RSA.importKey(requests.get(HEALTH_SERVICE_API_URL + "/v2/auth/cert", verify=False).text.strip()) + rsa_public_key = PKCS1_OAEP.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": @@ -20,11 +32,11 @@ def __init__(self, gateway, token): 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 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_auth_header(self, headers): token = cache.get(ABDM_TOKEN_CACHE_KEY) @@ -39,7 +51,7 @@ def add_auth_header(self, headers): "Accept": "application/json", } resp = requests.post( - ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers + ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers, verify=False ) print("Token Response Status: {}".format(resp.status_code)) if resp.status_code < 300: @@ -70,7 +82,7 @@ def get(self, path, params=None): url = self.url + path headers = {} headers = self.add_auth_header(headers) - response = requests.get(url, headers=headers, params=params) + response = requests.get(url, headers=headers, params=params, verify=False) return response def post(self, path, data=None): @@ -87,7 +99,7 @@ def post(self, path, data=None): data_json = json.dumps(data) # print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) print("Posting Request to: {}".format(url)) - response = requests.post(url, headers=headers, data=data_json) + response = requests.post(url, headers=headers, data=data_json, verify=False) print("{} Response: {}".format(response.status_code, response.text)) return response @@ -135,7 +147,16 @@ def __init__(self): self.api = APIGateway("health", None) # V2 APIs - def check_and_generate_mobile_otp(self, data): - path = "/v2/registration/aadhaar/checkAndGenerateMobileOTP" + 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_mobile_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() From 1307f0049bd16f8e9a69d2278829e00b1a625d67 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 2 Jan 2023 11:33:11 +0300 Subject: [PATCH 008/137] Skip SSL Verification --- care/abdm/utils/api_call.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index a793650cfb..56c2b77e45 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -12,6 +12,7 @@ # TODO: Exception handling for all api calls, need to gracefully handle known exceptions + class APIGateway: def __init__(self, gateway, token): if gateway == "health": @@ -39,7 +40,10 @@ def add_auth_header(self, headers): "Accept": "application/json", } resp = requests.post( - ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers + ABDM_TOKEN_URL, + data=json.dumps(data), + headers=auth_headers, + verify=False, ) print("Token Response Status: {}".format(resp.status_code)) if resp.status_code < 300: @@ -70,7 +74,7 @@ def get(self, path, params=None): url = self.url + path headers = {} headers = self.add_auth_header(headers) - response = requests.get(url, headers=headers, params=params) + response = requests.get(url, headers=headers, params=params, verify=False) return response def post(self, path, data=None): @@ -87,7 +91,7 @@ def post(self, path, data=None): data_json = json.dumps(data) # print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) print("Posting Request to: {}".format(url)) - response = requests.post(url, headers=headers, data=data_json) + response = requests.post(url, headers=headers, data=data_json, verify=False) print("{} Response: {}".format(response.status_code, response.text)) return response From 430ef5c19051cdab139fe9c8314f37e405c6111a Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Mon, 2 Jan 2023 18:46:18 +0530 Subject: [PATCH 009/137] Fix V2 API's --- care/abdm/api/viewsets/healthid.py | 4 ++-- care/abdm/utils/api_call.py | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 63577d83bb..9ece094ae2 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -37,7 +37,7 @@ def generate_aadhaar_otp(self, request): data = request.data serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGatewayV2().generate_aadhaar_otp(data) + response = HealthIdGatewayV2().generate_document_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( @@ -67,7 +67,7 @@ def verify_aadhaar_otp(self, request): data = request.data serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGateway().verify_aadhaar_otp(data) + response = HealthIdGatewayV2().verify_document_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index b5cccd5a39..40e1c07415 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -11,16 +11,15 @@ # TODO: Exception handling for all api calls, need to gracefully handle known exceptions -from Crypto.Cipher import PKCS1_OAEP +from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA from base64 import b64encode def encrypt_with_public_key(a_message): rsa_public_key = RSA.importKey(requests.get(HEALTH_SERVICE_API_URL + "/v2/auth/cert", verify=False).text.strip()) - rsa_public_key = PKCS1_OAEP.new(rsa_public_key) + rsa_public_key = PKCS1_v1_5.new(rsa_public_key) encrypted_text = rsa_public_key.encrypt(a_message.encode()) - return b64encode(encrypted_text).decode() @@ -154,9 +153,16 @@ def generate_aadhaar_otp(self, data): response = self.api.post(path, data) return response.json() - def generate_mobile_otp(self, data): - path = "/v2/registration/aadhaar/generateOtp" - data["aadhaar"] = encrypt_with_public_key(data["aadhaar"]) + 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() From 6014b8bcc5f8f47af67db7197ed46a0ed0c1f8b1 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Mon, 2 Jan 2023 19:50:20 +0530 Subject: [PATCH 010/137] Fix V2 API's --- care/abdm/api/serializers/abha.py | 0 care/abdm/api/viewsets/abha.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 care/abdm/api/serializers/abha.py create mode 100644 care/abdm/api/viewsets/abha.py diff --git a/care/abdm/api/serializers/abha.py b/care/abdm/api/serializers/abha.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py new file mode 100644 index 0000000000..e69de29bb2 From 99ac633b6481e695e5ad0b3eff897f01c9d0e949 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Mon, 2 Jan 2023 19:50:49 +0530 Subject: [PATCH 011/137] Added More Views --- care/abdm/api/serializers/abha.py | 9 +++++++++ care/abdm/api/viewsets/abha.py | 26 ++++++++++++++++++++++++++ config/api_router.py | 2 ++ 3 files changed, 37 insertions(+) diff --git a/care/abdm/api/serializers/abha.py b/care/abdm/api/serializers/abha.py index e69de29bb2..c4d88dbc0f 100644 --- a/care/abdm/api/serializers/abha.py +++ 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/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index e69de29bb2..9b2e112354 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -0,0 +1,26 @@ +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 +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.utils.queryset.patient import get_patient_queryset + + +class AbhaViewSet(GenericViewSet): + serializer_class = AbhaSerializer + model = AbhaNumber + queryset = AbhaNumber.objects.all() + + def get_abha_object(self): + queryset = get_patient_queryset(self.request.user) + patient_obj = get_object_or_404(queryset.filter.filter( + external_id=self.kwargs.get("patient_external_id") + )) + return patient_obj.abha_number + + @action(detail=False, methods=["POST"]) + def something_here(self, request, *args, **kwargs): + obj = self.get_abha_object() + return Response({}) diff --git a/config/api_router.py b/config/api_router.py index 8fbc3bd85c..a6abe88f5c 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -3,6 +3,7 @@ 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, @@ -184,6 +185,7 @@ 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" From 586ba6506bc99f14bfaf4cef5be9430d84cdcc6d Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 2 Jan 2023 18:44:04 +0300 Subject: [PATCH 012/137] Add ABHA_obj;Ignore HealthID --- care/abdm/api/serializers/abhanumber.py | 10 ++++++++++ care/abdm/utils/api_call.py | 2 ++ care/facility/api/serializers/patient.py | 17 +++++++++++------ 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 care/abdm/api/serializers/abhanumber.py 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/utils/api_call.py b/care/abdm/utils/api_call.py index 56c2b77e45..e5b0b9f754 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -130,6 +130,8 @@ def verify_mobile_otp(self, data): # /v1/registration/aadhaar/createHealthIdWithPreVerified def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" + print("Creating Health ID with data: {}".format(data)) + data.pop("healthId", None) response = self.api.post(path, data) return response.json() diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 5a0bdbd7c5..91b3d5233e 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -4,6 +4,7 @@ from django.utils.timezone import localtime, make_aware, now from rest_framework import serializers +from care.abdm.api.serializers.abhanumber import AbhaNumberSerializer from care.facility.api.serializers import TIMESTAMP_FIELDS from care.facility.api.serializers.facility import ( FacilityBasicInfoSerializer, @@ -132,7 +133,9 @@ class Meta: phone_number = PhoneNumberIsPossibleField() - facility = ExternalIdSerializerField(queryset=Facility.objects.all(), required=False) + facility = ExternalIdSerializerField( + queryset=Facility.objects.all(), required=False + ) medical_history = serializers.ListSerializer( child=MedicalHistorySerializer(), required=False ) @@ -177,6 +180,8 @@ class Meta: queryset=User.objects.all(), required=False, allow_null=True ) + abha_number_object = AbhaNumberSerializer(source="abha_number", read_only=True) + class Meta: model = PatientRegistration exclude = ( @@ -386,11 +391,11 @@ class PatientSearchSerializer(serializers.ModelSerializer): class Meta: model = PatientSearch exclude = ( - "date_of_birth", - "year_of_birth", - "external_id", - "id", - ) + TIMESTAMP_FIELDS + "date_of_birth", + "year_of_birth", + "external_id", + "id", + ) + TIMESTAMP_FIELDS class PatientTransferSerializer(serializers.ModelSerializer): From a4c9d2a4cf6f31309bd1dfc731c4808484c9df97 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Thu, 5 Jan 2023 10:23:19 +0300 Subject: [PATCH 013/137] Add APIs --- care/abdm/api/serializers/healthid.py | 31 ++++++++- care/abdm/api/viewsets/abha.py | 19 ++++-- care/abdm/api/viewsets/healthid.py | 23 ++++++- care/abdm/utils/api_call.py | 94 +++++++++++++++++++++++++-- 4 files changed, 154 insertions(+), 13 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 0a164a5b1f..daa00189fa 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -21,6 +21,32 @@ class AadharOtpResendRequestPayloadSerializer(Serializer): ) +class HealthIdSerializer(Serializer): + health_id = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Health ID", + ) + + +# "gender": "M", +# "mobile": "9545812125", +# "name": "suraj singh karki", +# "yearOfBirth": "1994" + + +class ABHASearchRequestSerializer: + name = CharField(max_length=64, min_length=1, required=False, help_text="Name") + mobile = CharField( + max_length=10, min_length=10, required=False, help_text="Mobile Number" + ) + gender = CharField(max_length=1, min_length=1, required=False, help_text="Gender") + yearOfBirth = CharField( + max_length=4, min_length=4, required=False, help_text="Year of Birth" + ) + + class GenerateMobileOtpRequestPayloadSerializer(Serializer): mobile = CharField( max_length=10, @@ -80,5 +106,6 @@ class CreateHealthIdSerializer(Serializer): help_text="PreVerified Transaction ID", validators=[], ) - patientId = CharField(required=True, help_text="Patient ID to be linked", - validators=[]) # TODO: Add UUID Validation + patientId = CharField( + required=True, help_text="Patient ID to be linked", validators=[] + ) # TODO: Add UUID Validation diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index 9b2e112354..649de4c01d 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -5,6 +5,7 @@ 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 @@ -15,12 +16,18 @@ class AbhaViewSet(GenericViewSet): def get_abha_object(self): queryset = get_patient_queryset(self.request.user) - patient_obj = get_object_or_404(queryset.filter.filter( - external_id=self.kwargs.get("patient_external_id") - )) + print( + "Finding patient with external_id: ", self.kwargs.get("patient_external_id") + ) + 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=["POST"]) - def something_here(self, request, *args, **kwargs): + @action(detail=False, methods=["GET"]) + def get_qr_code(self, request, *args, **kwargs): obj = self.get_abha_object() - return Response({}) + gateway = HealthIdGateway() + # Empty Dict as data, obj.access_token as auth + response = gateway.get_qr_code(obj) + return Response(response) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 9ece094ae2..43c30e05b9 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -13,6 +13,7 @@ AadharOtpResendRequestPayloadSerializer, CreateHealthIdSerializer, GenerateMobileOtpRequestPayloadSerializer, + HealthIdSerializer, VerifyOtpRequestPayloadSerializer, ) from care.abdm.models import AbhaNumber @@ -118,7 +119,9 @@ def create_health_id(self, request): if not patient_obj: raise ValidationError({"patient": "Not Found"}) response = HealthIdGateway().create_health_id(data) - abha_object = AbhaNumber.objects.filter(abha_number=response["healthIdNumber"]).first() + abha_object = AbhaNumber.objects.filter( + abha_number=response["healthIdNumber"] + ).first() if abha_object: # Flow when abha number exists in db somehow! pass @@ -141,6 +144,24 @@ def create_health_id(self, request): patient_obj.save() return Response(response, status=status.HTTP_200_OK) + # APIs to Find & Link Existing HealthID + # searchByHealthId + @swagger_auto_schema( + # /v1/registration/aadhaar/searchByHealthId + operation_id="search_by_health_id", + request_body=HealthIdSerializer, + responses={"200": "{'status': 'boolean'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def search_by_health_id(self, request): + data = request.data + 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) + + ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( # /v1/registration/aadhaar/checkAndGenerateMobileOTP diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index eeb0745c23..edde98ec81 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -30,6 +30,8 @@ class APIGateway: def __init__(self, gateway, token): if gateway == "health": self.url = HEALTH_SERVICE_API_URL + elif gateway == "abdm": + self.url = GATEWAY_API_URL else: self.url = GATEWAY_API_URL self.token = token @@ -40,10 +42,19 @@ def __init__(self, gateway, token): # 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": user_token, + } + ) + return headers + def add_auth_header(self, headers): token = cache.get(ABDM_TOKEN_CACHE_KEY) - print("Cached Token: {}".format(token)) + print("Using Cached Token") if not token: + print("No Token in Cache") data = { "clientId": settings.ABDM_CLIENT_ID, "clientSecret": settings.ABDM_CLIENT_SECRET, @@ -79,18 +90,23 @@ def add_auth_header(self, headers): else: print("Bad Response: {}".format(resp.text)) return None - print("Returning Authorization Header: Bearer {}".format(token)) + # print("Returning Authorization Header: Bearer {}".format(token)) + print("Adding Authorization Header") auth_header = {"Authorization": "Bearer {}".format(token)} return {**headers, **auth_header} - def get(self, path, params=None): + 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) + print("Making GET Request to: {}".format(url)) response = requests.get(url, headers=headers, params=params, verify=False) + print("{} Response: {}".format(response.status_code, response.text)) return response - def post(self, path, data=None): + def post(self, path, data=None, auth=None): url = self.url + path headers = { "Content-Type": "application/json", @@ -98,6 +114,8 @@ def post(self, path, data=None): "Accept-Language": "en-US", } headers = self.add_auth_header(headers) + if auth: + headers = self.add_user_header(headers, auth) # headers_string = " ".join( # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] # ) @@ -148,6 +166,63 @@ def create_health_id(self, data): 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/generate/access-token + def generate_access_token(self, data): + path = "/v1/auth/generate/access-token" + print("Generating Access Token for: {}".format(data["abha_number"])) + response = self.api.post(path, {"refreshToken": data["refresh_token"]}) + return response.json() + + # 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/qrCode + def get_qr_code(self, data, auth): + path = "/v1/account/qrCode" + access_token = self.generate_access_token(data) + print("Getting QR Code for: {}".format(data)) + response = self.api.get(path, {}, access_token) + print("QR Code Response: {}".format(response.text)) + return response.json() + class HealthIdGatewayV2: def __init__(self): @@ -174,3 +249,14 @@ def verify_document_mobile_otp(self, data): data.pop("cancelToken", {}) response = self.api.post(path, data) return response.json() + + +class AbdmGateway: + def __init__(self): + self.api = APIGateway("abdm", None) + + # /v0.5/users/auth/fetch-modes + def fetch_modes(self, data): + path = "/v0.5/users/auth/fetch-modes" + response = self.api.post(path, data) + return response.json() From b636084071eaf7ba6b7e0e9d915ca168374516d1 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Thu, 5 Jan 2023 11:24:29 +0300 Subject: [PATCH 014/137] Add Init & Verify APIs --- care/abdm/api/serializers/auth.py | 11 +++++++ care/abdm/api/serializers/healthid.py | 19 +++++++++++ care/abdm/api/viewsets/healthid.py | 47 +++++++++++++++++++++++++++ care/abdm/utils/api_call.py | 18 ++++++++++ 4 files changed, 95 insertions(+) diff --git a/care/abdm/api/serializers/auth.py b/care/abdm/api/serializers/auth.py index 6c4180aa8d..9b533d0c9b 100644 --- a/care/abdm/api/serializers/auth.py +++ b/care/abdm/api/serializers/auth.py @@ -11,3 +11,14 @@ class AbdmAuthResponseSerializer(Serializer): 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 index daa00189fa..e5528ed97a 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -30,6 +30,25 @@ class HealthIdSerializer(Serializer): ) +# { +# "authMethod": "AADHAAR_OTP", +# "healthid": "43-4221-5105-6749" +# } +class HealthIdAuthSerializer(Serializer): + auth_method = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Auth Method", + ) + healthid = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Health ID", + ) + + # "gender": "M", # "mobile": "9545812125", # "name": "suraj singh karki", diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 43c30e05b9..e664c954d0 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -13,6 +13,7 @@ AadharOtpResendRequestPayloadSerializer, CreateHealthIdSerializer, GenerateMobileOtpRequestPayloadSerializer, + HealthIdAuthSerializer, HealthIdSerializer, VerifyOtpRequestPayloadSerializer, ) @@ -161,6 +162,52 @@ def search_by_health_id(self, request): response = HealthIdGateway().search_by_health_id(data) return Response(response, status=status.HTTP_200_OK) + # auth/init + @swagger_auto_schema( + # /v1/auth/init + operation_id="auth_init", + request_body=HealthIdAuthSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def auth_init(self, request): + data = request.data + 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 + @swagger_auto_schema( + operation_id="confirm_with_aadhaar_otp", + request_body=VerifyOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def confirm_with_aadhaar_otp(self, request): + data = request.data + serializer = VerifyOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().confirm_with_aadhaar_otp(data) + return Response(response, status=status.HTTP_200_OK) + + # /v1/auth/confirmWithMobileOtp + @swagger_auto_schema( + operation_id="confirm_with_mobile_otp", + request_body=VerifyOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def confirm_with_mobile_otp(self, request): + data = request.data + serializer = VerifyOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().confirm_with_mobile_otp(data) + return Response(response, status=status.HTTP_200_OK) + ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index edde98ec81..6961485fc1 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -198,6 +198,24 @@ def search_by_mobile(self, data): # 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/generate/access-token def generate_access_token(self, data): path = "/v1/auth/generate/access-token" From 7f6464deaca09dee06ed26f0366246dfd12873be Mon Sep 17 00:00:00 2001 From: Gigin George Date: Thu, 5 Jan 2023 11:40:48 +0300 Subject: [PATCH 015/137] Fix SearchByHealthId Serializer --- care/abdm/api/serializers/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index e5528ed97a..4c0a477c6b 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -22,7 +22,7 @@ class AadharOtpResendRequestPayloadSerializer(Serializer): class HealthIdSerializer(Serializer): - health_id = CharField( + healthId = CharField( max_length=64, min_length=1, required=True, @@ -41,7 +41,7 @@ class HealthIdAuthSerializer(Serializer): required=True, help_text="Auth Method", ) - healthid = CharField( + healthId = CharField( max_length=64, min_length=1, required=True, From 8ec246f77fe33183719e79d9fe551668f2ed5b6e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 5 Jan 2023 19:48:18 +0530 Subject: [PATCH 016/137] renamed attributes in HealthIdAuthSerializer --- care/abdm/api/serializers/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 4c0a477c6b..736da66b94 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -35,13 +35,13 @@ class HealthIdSerializer(Serializer): # "healthid": "43-4221-5105-6749" # } class HealthIdAuthSerializer(Serializer): - auth_method = CharField( + authMethod = CharField( max_length=64, min_length=1, required=True, help_text="Auth Method", ) - healthId = CharField( + healthid = CharField( max_length=64, min_length=1, required=True, From 3abbf8d58cb154d920c8d231b2ce42d864652483 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 6 Jan 2023 01:16:47 +0530 Subject: [PATCH 017/137] fixed get_profile api in abdm --- care/abdm/api/serializers/healthid.py | 3 ++ care/abdm/api/viewsets/healthid.py | 61 ++++++++++++++++++++++++++- care/abdm/utils/api_call.py | 20 +++++++-- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 736da66b94..3c93bf8647 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -98,6 +98,9 @@ class VerifyOtpRequestPayloadSerializer(Serializer): help_text="Transaction ID", validators=[], ) + patientId = CharField( + required=True, help_text="Patient ID to be linked", validators=[] + ) # TODO: Add UUID Validation # { diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index e664c954d0..15497e5d2f 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -102,6 +102,29 @@ def verify_mobile_otp(self, request): response = HealthIdGateway().verify_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) + def add_abha_details_to_patient(self, data, patient_obj): + abha_object = AbhaNumber.objects.filter( + abha_number=data["healthIdNumber"] + ).first() + if abha_object: + # Flow when abha number exists in db somehow! + return False + else: + # Create abha number flow + abha_object = AbhaNumber() + abha_object.abha_number = data["healthIdNumber"] + abha_object.email = data["email"] + abha_object.first_name = data["firstName"] + abha_object.health_id = data["healthId"] + abha_object.last_name = data["lastName"] + abha_object.middle_name = data["middleName"] + abha_object.profile_photo = data["profilePhoto"] + abha_object.save() + + patient_obj.abha_number = abha_object + patient_obj.save() + return True + @swagger_auto_schema( # /v1/registration/aadhaar/createHealthId operation_id="create_health_id", @@ -191,7 +214,24 @@ def confirm_with_aadhaar_otp(self, request): serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_aadhaar_otp(data) - return Response(response, status=status.HTTP_200_OK) + abha_object = HealthIdGateway().get_profile(response) + + 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 self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response(abha_object, status=status.HTTP_200_OK) + else: + return Response( + {"message": "ABHA NUmber / Health ID already Exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) # /v1/auth/confirmWithMobileOtp @swagger_auto_schema( @@ -206,7 +246,24 @@ def confirm_with_mobile_otp(self, request): serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) + abha_object = HealthIdGateway().get_profile(response) + + 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 self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response(abha_object, status=status.HTTP_200_OK) + else: + return Response( + {"message": "ABHA NUmber / Health ID already Exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) ############################################################################################################ # HealthID V2 APIs diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 6961485fc1..1635b498e6 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -45,7 +45,7 @@ def __init__(self, gateway, token): def add_user_header(self, headers, user_token): headers.update( { - "X-Token": user_token, + "X-Token": "Bearer " + user_token, } ) return headers @@ -218,10 +218,22 @@ def confirm_with_mobile_otp(self, data): # /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" - print("Generating Access Token for: {}".format(data["abha_number"])) - response = self.api.post(path, {"refreshToken": data["refresh_token"]}) - return response.json() + response = self.api.post(path, {"refreshToken": refreshToken}) + return response.json()["accessToken"] # Account APIs From 5a80d18e1ea5e1dfdd510d072cd356e3dc8062e0 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 9 Jan 2023 12:02:40 +0530 Subject: [PATCH 018/137] Add get_profile API --- care/abdm/api/viewsets/abha.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index 649de4c01d..d6eba3fe8e 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -31,3 +31,11 @@ def get_qr_code(self, request, *args, **kwargs): # Empty Dict as data, obj.access_token as auth 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() + # Empty Dict as data, obj.access_token as auth + response = gateway.get_profile(obj) + return Response(response) From 9e65e94b6a93b22cc4c56c8c27f941084a295661 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 9 Jan 2023 18:59:47 +0530 Subject: [PATCH 019/137] fixed aadhaar otp api call --- care/abdm/api/serializers/healthid.py | 2 +- care/abdm/api/viewsets/healthid.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 3c93bf8647..44b5f1d0c5 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -99,7 +99,7 @@ class VerifyOtpRequestPayloadSerializer(Serializer): validators=[], ) patientId = CharField( - required=True, help_text="Patient ID to be linked", validators=[] + required=False, help_text="Patient ID to be linked", validators=[] ) # TODO: Add UUID Validation diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 15497e5d2f..37c0178e36 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -39,7 +39,7 @@ def generate_aadhaar_otp(self, request): data = request.data serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGatewayV2().generate_document_mobile_otp(data) + response = HealthIdGatewayV2().generate_aadhaar_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( From 2a5f1e0862d7064f2e0ecc30feccd06fcffedaf0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 11 Jan 2023 14:52:49 +0530 Subject: [PATCH 020/137] added abha_number to patient serializer --- care/abdm/api/serializers/healthid.py | 2 +- care/abdm/api/viewsets/healthid.py | 20 +++++++++++--------- care/facility/api/serializers/patient.py | 4 ++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 44b5f1d0c5..e8ca07d242 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -129,5 +129,5 @@ class CreateHealthIdSerializer(Serializer): validators=[], ) patientId = CharField( - required=True, help_text="Patient ID to be linked", validators=[] + required=False, help_text="Patient ID to be linked", validators=[] ) # TODO: Add UUID Validation diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 37c0178e36..324a0123fa 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -69,7 +69,9 @@ def verify_aadhaar_otp(self, request): data = request.data serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGatewayV2().verify_document_mobile_otp(data) + response = HealthIdGateway().verify_aadhaar_otp( + data + ) # HealthIdGatewayV2().verify_document_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( @@ -137,11 +139,11 @@ def create_health_id(self, request): data = request.data serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) - 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"}) + # 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"}) response = HealthIdGateway().create_health_id(data) abha_object = AbhaNumber.objects.filter( abha_number=response["healthIdNumber"] @@ -164,9 +166,9 @@ def create_health_id(self, request): abha_object.refresh_token = data["txnId"] abha_object.save() - patient_obj.abha_number = abha_object - patient_obj.save() - return Response(response, status=status.HTTP_200_OK) + # patient_obj.abha_number = abha_object + # patient_obj.save() + return Response({"abha": abha_object.external_id}, status=status.HTTP_200_OK) # APIs to Find & Link Existing HealthID # searchByHealthId diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 91b3d5233e..2852b0cbe7 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -5,6 +5,7 @@ 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, @@ -180,6 +181,9 @@ class Meta: queryset=User.objects.all(), required=False, allow_null=True ) + abha_number = ExternalIdSerializerField( + queryset=AbhaNumber.objects.all(), required=False + ) abha_number_object = AbhaNumberSerializer(source="abha_number", read_only=True) class Meta: From 9478eca9be9c6771fc449960a2b04dc59df87120 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 29 Jan 2023 13:53:55 +0530 Subject: [PATCH 021/137] send entire response after health id creation --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 324a0123fa..d9353bd55c 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -168,7 +168,7 @@ def create_health_id(self, request): # patient_obj.abha_number = abha_object # patient_obj.save() - return Response({"abha": abha_object.external_id}, status=status.HTTP_200_OK) + return Response(response, status=status.HTTP_200_OK) # APIs to Find & Link Existing HealthID # searchByHealthId From 076df0747b1cc75c6b9c2e4e67b4f595c7fdf950 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 30 Jan 2023 17:20:28 +0530 Subject: [PATCH 022/137] feat: added share_profile endpoint --- care/abdm/api/serializers/healthid.py | 31 ++++ care/abdm/api/serializers/hip.py | 33 +++++ care/abdm/api/viewsets/healthid.py | 33 +++++ care/abdm/api/viewsets/hip.py | 132 ++++++++++++++++++ care/abdm/utils/api_call.py | 6 + .../migrations/0331_auto_20230130_1652.py | 18 +++ care/facility/models/patient.py | 6 +- config/api_router.py | 2 + 8 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 care/abdm/api/serializers/hip.py create mode 100644 care/abdm/api/viewsets/hip.py create mode 100644 care/facility/migrations/0331_auto_20230130_1652.py diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index e8ca07d242..4701170711 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -103,6 +103,37 @@ class VerifyOtpRequestPayloadSerializer(Serializer): ) # TODO: Add UUID Validation +class VerifyDemographicsRequestPayloadSerializer(Serializer): + gender = CharField( + max_length=10, + min_length=1, + required=True, + help_text="Gender", + validators=[], + ) + name = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Name", + validators=[], + ) + yearOfBirth = CharField( + max_length=4, + min_length=4, + required=True, + help_text="Year Of Birth", + validators=[], + ) + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Transaction ID", + validators=[], + ) + + # { # "email": "Example@Demo.com", # "firstName": "manoj", 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/healthid.py b/care/abdm/api/viewsets/healthid.py index d9353bd55c..83ff1708a2 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -15,6 +15,7 @@ GenerateMobileOtpRequestPayloadSerializer, HealthIdAuthSerializer, HealthIdSerializer, + VerifyDemographicsRequestPayloadSerializer, VerifyOtpRequestPayloadSerializer, ) from care.abdm.models import AbhaNumber @@ -267,6 +268,38 @@ def confirm_with_mobile_otp(self, request): status=status.HTTP_400_BAD_REQUEST, ) + @swagger_auto_schema( + operation_id="confirm_with_demographics", + request_body=VerifyDemographicsRequestPayloadSerializer, + responses={"200": "{'status': true}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def confirm_with_demographics(self, request): + data = request.data + serializer = VerifyDemographicsRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().confirm_with_demographics(data) + return Response(response, status=status.HTTP_200_OK) + + # patient_id = data.pop("patientId") + # if patient_id and response.status: + # 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 self.add_abha_details_to_patient( + # abha_object, + # patient_obj, + # ): + # return Response(abha_object, status=status.HTTP_200_OK) + # else: + # return Response( + # {"message": "ABHA NUmber / Health ID already Exists"}, + # status=status.HTTP_400_BAD_REQUEST, + # ) + ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py new file mode 100644 index 0000000000..75500438e0 --- /dev/null +++ b/care/abdm/api/viewsets/hip.py @@ -0,0 +1,132 @@ +import datetime + +from rest_framework import status +from rest_framework.decorators import action +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 HealthIdGateway +from care.facility.models.facility import Facility +from care.facility.models.patient import PatientRegistration + + +class HipViewSet(GenericViewSet): + def add_abha_details_to_patient(self, data, patient_obj): + abha_object = AbhaNumber.objects.filter( + abha_number=data["healthIdNumber"] + ).first() + if abha_object: + # Flow when abha number exists in db somehow! + pass + else: + # Create abha number flow + abha_object = AbhaNumber() + abha_object.abha_number = data["healthIdNumber"] + # abha_object.email = data["email"] + # abha_object.first_name = data["firstName"] + abha_object.health_id = data["healthId"] + # abha_object.last_name = data["lastName"] + # abha_object.middle_name = data["middleName"] + # abha_object.profile_photo = data["profilePhoto"] + abha_object.save() + + patient_obj.abha_number = abha_object + patient_obj.save() + return True + + def demographics_verification(self, data): + auth_init_response = HealthIdGateway().auth_init( + {"authMethod": "DEMOGRAPHICS", "healthid": data["healthIdNumber"]} + ) + if "txnId" in auth_init_response: + demographics_response = HealthIdGateway().confirm_with_demographics( + { + "txnId": auth_init_response["txnId"], + "name": data["name"], + "gender": data["gender"], + "yearOfBirth": data["yearOfBirth"], + } + ) + return "status" in demographics_response and demographics_response["status"] + else: + return False + + @action(detail=False, methods=["POST"]) + def share_profile(self, request, *args, **kwargs): + data = request.data + patient_data = data["profile"]["patient"] + # hip_id = self.request.GET.get("hip_id") + counter_id = self.request.GET.get("counter_id") # facility_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) + + # create a patient or search for existing patient with this abha number + patient = PatientRegistration.objects.filter( + abha_number__abha_number=patient_data["healthIdNumber"] + ) + if not patient: + patient = PatientRegistration.objects.create( + facility=Facility.objects.get(external_id=counter_id), + name=patient_data["name"], + gender=1 + if patient_data["gender"] == "M" + else 2 + if patient_data["gender"] == "F" + else 3, + is_antenatal=False, + phone_number=patient_data["mobile"], + emergency_phone_number=patient_data["mobile"], + date_of_birth=datetime.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"], + created_by=None, + state=None, + district=None, + local_body=None, + ward=None, + ) + + # verify details using demographics method (name, gender and yearOfBirth) + if self.demographics_verification(patient_data): + self.add_abha_details_to_patient(patient_data, patient) + return Response( + { + "requestId": data["requestId"], + "timestamp": str(datetime.datetime.now()), + "acknowledgement": { + "status": "SUCCESS", + "healthId": patient_data["healthId"], + "healthIdNumber": patient_data["healthIdNumber"], + "tokenNumber": "01", # this is for out patients + }, + }, + status=status.HTTP_202_ACCEPTED, + ) + else: + return Response( + { + "requestId": data["requestId"], + "timestamp": str(datetime.datetime.now()), + "acknowledgement": { + "status": "FAILURE", + }, + "error": { + "code": 1000, + "message": "Demographics verification failed", + }, + }, + status=status.HTTP_401_UNAUTHORIZED, + ) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 1635b498e6..afb3f1e1f9 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -216,6 +216,12 @@ def confirm_with_mobile_otp(self, data): 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() + # /v1/auth/generate/access-token def generate_access_token(self, data): if "access_token" in data: diff --git a/care/facility/migrations/0331_auto_20230130_1652.py b/care/facility/migrations/0331_auto_20230130_1652.py new file mode 100644 index 0000000000..7b96181775 --- /dev/null +++ b/care/facility/migrations/0331_auto_20230130_1652.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2023-01-30 11:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0330_auto_20221220_2312'), + ] + + operations = [ + migrations.AlterField( + model_name='patientsearch', + name='state_id', + field=models.IntegerField(null=True), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 6997027460..b4daf34522 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -394,7 +394,9 @@ class TestTypeEnum(enum.Enum): # ABDM Health ID - abha_number = models.OneToOneField(AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True) + abha_number = models.OneToOneField( + AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True + ) history = HistoricalRecords(excluded_fields=["patient_search_id", "meta_info"]) @@ -594,7 +596,7 @@ class PatientSearch(PatientBaseModel): phone_number = models.CharField(max_length=14) date_of_birth = models.DateField(null=True) year_of_birth = models.IntegerField() - state_id = models.IntegerField() + state_id = models.IntegerField(null=True) facility = models.ForeignKey("Facility", on_delete=models.SET_NULL, null=True) patient_external_id = EncryptedCharField(max_length=100, default="") diff --git a/config/api_router.py b/config/api_router.py index a6abe88f5c..014973d2d7 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -5,6 +5,7 @@ from care.abdm.api.viewsets.abha import AbhaViewSet from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet +from care.abdm.api.viewsets.hip import HipViewSet from care.facility.api.viewsets.ambulance import ( AmbulanceCreateViewSet, AmbulanceViewSet, @@ -198,6 +199,7 @@ # ABDM endpoints router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") +router.register("abdm/hip", HipViewSet, basename="hip") app_name = "api" urlpatterns = [ From 291074446c798d0ad9252b8fe8003a2aa9aa2d22 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 1 Feb 2023 07:38:23 +0530 Subject: [PATCH 023/137] changed endpoint of abdm-hip-share --- care/abdm/api/viewsets/hip.py | 111 ++++++++++++++++++++-------------- care/abdm/utils/api_call.py | 19 +++++- config/api_router.py | 15 ++++- config/urls.py | 1 + 4 files changed, 97 insertions(+), 49 deletions(-) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 75500438e0..93c2a23556 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -2,17 +2,21 @@ from rest_framework import status from rest_framework.decorators import action +from rest_framework.permissions import AllowAny 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 HealthIdGateway +from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration class HipViewSet(GenericViewSet): + permission_classes = (AllowAny,) + authentication_classes = [] + def add_abha_details_to_patient(self, data, patient_obj): abha_object = AbhaNumber.objects.filter( abha_number=data["healthIdNumber"] @@ -54,11 +58,10 @@ def demographics_verification(self, data): return False @action(detail=False, methods=["POST"]) - def share_profile(self, request, *args, **kwargs): + def share(self, request, *args, **kwargs): data = request.data patient_data = data["profile"]["patient"] - # hip_id = self.request.GET.get("hip_id") - counter_id = self.request.GET.get("counter_id") # facility_id + counter_id = data["profile"]["hipCode"] patient_data["mobile"] = "" for identifier in patient_data["identifiers"]: @@ -72,61 +75,77 @@ def share_profile(self, request, *args, **kwargs): patient = PatientRegistration.objects.filter( abha_number__abha_number=patient_data["healthIdNumber"] ) - if not patient: - patient = PatientRegistration.objects.create( - facility=Facility.objects.get(external_id=counter_id), - name=patient_data["name"], - gender=1 - if patient_data["gender"] == "M" - else 2 - if patient_data["gender"] == "F" - else 3, - is_antenatal=False, - phone_number=patient_data["mobile"], - emergency_phone_number=patient_data["mobile"], - date_of_birth=datetime.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"], - created_by=None, - state=None, - district=None, - local_body=None, - ward=None, + if patient: + res = AbdmGateway().on_share( + { + "requestId": data["requestId"], + # "timestamp": str(datetime.datetime.now()), + "acknowledgement": { + "status": "SUCCESS", + "healthId": patient_data["healthId"], + "tokenNumber": "02", + }, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["requestId"]}, + } + ) + print(res) + + return Response( + { + "requestId": data["requestId"], + "status": "SUCCESS", + "healthId": patient_data["healthIdNumber"], + # "healthIdNumber": patient_data["healthIdNumber"], + "tokenNumber": "02", # this is for out patients + }, + status=status.HTTP_202_ACCEPTED, ) + patient = PatientRegistration.objects.create( + facility=Facility.objects.get(external_id=counter_id), + name=patient_data["name"], + gender=1 + if patient_data["gender"] == "M" + else 2 + if patient_data["gender"] == "F" + else 3, + is_antenatal=False, + phone_number=patient_data["mobile"], + emergency_phone_number=patient_data["mobile"], + date_of_birth=datetime.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"], + created_by=None, + state=None, + district=None, + local_body=None, + ward=None, + ) + # verify details using demographics method (name, gender and yearOfBirth) if self.demographics_verification(patient_data): self.add_abha_details_to_patient(patient_data, patient) return Response( { - "requestId": data["requestId"], - "timestamp": str(datetime.datetime.now()), - "acknowledgement": { - "status": "SUCCESS", - "healthId": patient_data["healthId"], - "healthIdNumber": patient_data["healthIdNumber"], - "tokenNumber": "01", # this is for out patients - }, + "status": "SUCCESS", + "healthId": patient_data["healthId"], + # "healthIdNumber": patient_data["healthIdNumber"], + "tokenNumber": "02", # this is for out patients }, status=status.HTTP_202_ACCEPTED, ) else: return Response( { - "requestId": data["requestId"], - "timestamp": str(datetime.datetime.now()), - "acknowledgement": { - "status": "FAILURE", - }, - "error": { - "code": 1000, - "message": "Demographics verification failed", - }, + "status": "FAILURE", + "healthId": patient_data["healthId"], + "healthIdNumber": patient_data["healthIdNumber"], }, status=status.HTTP_401_UNAUTHORIZED, ) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index afb3f1e1f9..9780473773 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -10,6 +10,7 @@ GATEWAY_API_URL = "https://dev.abdm.gov.in/" HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" +ABDM_GATEWAY_URL = GATEWAY_API_URL + "gateway" ABDM_TOKEN_CACHE_KEY = "abdm_token" # TODO: Exception handling for all api calls, need to gracefully handle known exceptions @@ -32,6 +33,8 @@ def __init__(self, gateway, token): 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 @@ -95,6 +98,9 @@ def add_auth_header(self, headers): 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 = {} @@ -106,7 +112,7 @@ def get(self, path, params=None, auth=None): print("{} Response: {}".format(response.status_code, response.text)) return response - def post(self, path, data=None, auth=None): + def post(self, path, data=None, auth=None, additional_headers=None): url = self.url + path headers = { "Content-Type": "application/json", @@ -116,6 +122,8 @@ def post(self, path, data=None, auth=None): 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()] # ) @@ -289,10 +297,17 @@ def verify_document_mobile_otp(self, data): class AbdmGateway: def __init__(self): - self.api = APIGateway("abdm", None) + self.api = APIGateway("abdm_gateway", None) # /v0.5/users/auth/fetch-modes def fetch_modes(self, data): path = "/v0.5/users/auth/fetch-modes" response = self.api.post(path, data) return response.json() + + # /v1.0/patients/profile/on-share + def on_share(self, data): + path = "/v1.0/patients/profile/on-share" + additional_headers = {"X-CM-ID": "sbx"} + response = self.api.post(path, data, None, additional_headers) + return response diff --git a/config/api_router.py b/config/api_router.py index 014973d2d7..ed6767ab22 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -82,10 +82,19 @@ from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet + +class OptionalSlashRouter(SimpleRouter): + def __init__(self): + super().__init__() + self.trailing_slash = "/?" + + if settings.DEBUG: router = DefaultRouter() + # abdm_router = DefaultRouter() else: router = SimpleRouter() +abdm_router = OptionalSlashRouter() router.register("users", UserViewSet) user_nested_rotuer = NestedSimpleRouter(router, r"users", lookup="users") @@ -199,7 +208,7 @@ # ABDM endpoints router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") -router.register("abdm/hip", HipViewSet, basename="hip") +abdm_router.register("profile", HipViewSet, basename="hip") app_name = "api" urlpatterns = [ @@ -211,3 +220,7 @@ url(r"^", include(resource_nested_router.urls)), url(r"^", include(shifting_nested_router.urls)), ] + +abdm_urlpatterns = [ + url(r"^", include(abdm_router.urls)), +] diff --git a/config/urls.py b/config/urls.py index 270d8bb96c..9fbfc17816 100644 --- a/config/urls.py +++ b/config/urls.py @@ -81,6 +81,7 @@ name="change_password_view", ), path("api/v1/", include(api_router.urlpatterns)), + path("v1.0/patients/", include(api_router.abdm_urlpatterns)), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), From 5d1e1fb81a7fd498880cee3e9206179cd7e635e5 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 3 Feb 2023 14:12:53 +0530 Subject: [PATCH 024/137] if patientId then link --- care/abdm/api/viewsets/healthid.py | 80 ++++++++++++++++-------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 83ff1708a2..4e4677a411 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -140,11 +140,6 @@ def create_health_id(self, request): data = request.data serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) - # 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"}) response = HealthIdGateway().create_health_id(data) abha_object = AbhaNumber.objects.filter( abha_number=response["healthIdNumber"] @@ -167,8 +162,15 @@ def create_health_id(self, request): abha_object.refresh_token = data["txnId"] abha_object.save() - # patient_obj.abha_number = abha_object - # patient_obj.save() + 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"}) + patient_obj.abha_number = abha_object + patient_obj.save() + return Response(response, status=status.HTTP_200_OK) # APIs to Find & Link Existing HealthID @@ -219,28 +221,29 @@ def confirm_with_aadhaar_otp(self, request): response = HealthIdGateway().confirm_with_aadhaar_otp(data) abha_object = HealthIdGateway().get_profile(response) - 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 "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 self.add_abha_details_to_patient( - abha_object, - patient_obj, - ): - return Response(abha_object, status=status.HTTP_200_OK) - else: - return Response( - {"message": "ABHA NUmber / Health ID already Exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if not self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response( + {"message": "ABHA NUmber / Health ID already Exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return Response(abha_object, status=status.HTTP_200_OK) # /v1/auth/confirmWithMobileOtp @swagger_auto_schema( operation_id="confirm_with_mobile_otp", request_body=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, + # responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @action(detail=False, methods=["post"]) @@ -251,22 +254,23 @@ def confirm_with_mobile_otp(self, request): response = HealthIdGateway().confirm_with_mobile_otp(data) abha_object = HealthIdGateway().get_profile(response) - 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 "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 self.add_abha_details_to_patient( - abha_object, - patient_obj, - ): - return Response(abha_object, status=status.HTTP_200_OK) - else: - return Response( - {"message": "ABHA NUmber / Health ID already Exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if not self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response( + {"message": "ABHA NUmber / Health ID already Exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return Response(abha_object, status=status.HTTP_200_OK) @swagger_auto_schema( operation_id="confirm_with_demographics", From 94ec9b3d55841c0abdc1320fefe1d99ebc70ff8f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 3 Feb 2023 18:18:36 +0530 Subject: [PATCH 025/137] added patient profile share --- care/abdm/api/viewsets/hip.py | 135 ++++++++++++++++------------------ 1 file changed, 62 insertions(+), 73 deletions(-) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 93c2a23556..424eb4dcbc 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -1,4 +1,5 @@ -import datetime +import uuid +from datetime import datetime, timezone from rest_framework import status from rest_framework.decorators import action @@ -60,6 +61,7 @@ def demographics_verification(self, data): @action(detail=False, methods=["POST"]) def share(self, request, *args, **kwargs): data = request.data + patient_data = data["profile"]["patient"] counter_id = data["profile"]["hipCode"] @@ -71,81 +73,68 @@ def share(self, request, *args, **kwargs): serializer = HipShareProfileSerializer(data=data) serializer.is_valid(raise_exception=True) - # create a patient or search for existing patient with this abha number - patient = PatientRegistration.objects.filter( - abha_number__abha_number=patient_data["healthIdNumber"] - ) - if patient: - res = AbdmGateway().on_share( - { - "requestId": data["requestId"], - # "timestamp": str(datetime.datetime.now()), - "acknowledgement": { - "status": "SUCCESS", - "healthId": patient_data["healthId"], - "tokenNumber": "02", - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["requestId"]}, - } + if self.demographics_verification(patient_data): + patient = PatientRegistration.objects.filter( + abha_number__abha_number=patient_data["healthIdNumber"] ) - print(res) - return Response( - { - "requestId": data["requestId"], - "status": "SUCCESS", - "healthId": patient_data["healthIdNumber"], - # "healthIdNumber": patient_data["healthIdNumber"], - "tokenNumber": "02", # this is for out patients - }, - status=status.HTTP_202_ACCEPTED, - ) + if not patient: + patient = PatientRegistration.objects.create( + facility=Facility.objects.get(external_id=counter_id), + name=patient_data["name"], + gender=1 + if patient_data["gender"] == "M" + else 2 + if patient_data["gender"] == "F" + else 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"], + created_by=None, + state=None, + district=None, + local_body=None, + ward=None, + ) + self.add_abha_details_to_patient(patient_data, patient) - patient = PatientRegistration.objects.create( - facility=Facility.objects.get(external_id=counter_id), - name=patient_data["name"], - gender=1 - if patient_data["gender"] == "M" - else 2 - if patient_data["gender"] == "F" - else 3, - is_antenatal=False, - phone_number=patient_data["mobile"], - emergency_phone_number=patient_data["mobile"], - date_of_birth=datetime.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"], - created_by=None, - state=None, - district=None, - local_body=None, - ward=None, - ) - - # verify details using demographics method (name, gender and yearOfBirth) - if self.demographics_verification(patient_data): - self.add_abha_details_to_patient(patient_data, patient) - return Response( - { + 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"], - # "healthIdNumber": patient_data["healthIdNumber"], - "tokenNumber": "02", # this is for out patients + "healthId": patient_data["healthId"] + or patient_data["healthIdNumber"], + "tokenNumber": "100", }, - status=status.HTTP_202_ACCEPTED, - ) - else: - return Response( - { - "status": "FAILURE", - "healthId": patient_data["healthId"], - "healthIdNumber": patient_data["healthIdNumber"], + "error": None, + "resp": { + "requestId": data["requestId"], }, - status=status.HTTP_401_UNAUTHORIZED, - ) + } + + on_share_response = AbdmGateway().on_share(payload) + if on_share_response.status_code == 202: + print("on_share_header", on_share_response.request.body) + return Response( + on_share_response.request.body, + status=status.HTTP_202_ACCEPTED, + ) + + return Response( + { + "status": "FAILURE", + "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], + }, + status=status.HTTP_401_UNAUTHORIZED, + ) From 8f32f83e19cf575d0bfbdd3a138462471f8dae8f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Feb 2023 23:58:46 +0530 Subject: [PATCH 026/137] changed response structure of abha_object --- care/abdm/api/viewsets/healthid.py | 123 ++++++++++++++--------------- 1 file changed, 58 insertions(+), 65 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 4e4677a411..50a496ee19 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -105,27 +105,33 @@ def verify_mobile_otp(self, request): response = HealthIdGateway().verify_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) - def add_abha_details_to_patient(self, data, patient_obj): + def create_abha(self, abha_profile): abha_object = AbhaNumber.objects.filter( - abha_number=data["healthIdNumber"] + abha_number=abha_profile["healthIdNumber"] ).first() + if abha_object: - # Flow when abha number exists in db somehow! + return abha_object + + abha_object = AbhaNumber() + abha_object.abha_number = abha_profile["healthIdNumber"] + abha_object.email = abha_profile["email"] + abha_object.first_name = abha_profile["firstName"] + abha_object.health_id = abha_profile["healthId"] + abha_object.last_name = abha_profile["lastName"] + abha_object.middle_name = abha_profile["middleName"] + abha_object.profile_photo = abha_profile["profilePhoto"] + abha_object.txn_id = abha_profile["healthIdNumber"] + abha_object.save() + + return abha_object + + def add_abha_details_to_patient(self, abha_object, patient_object): + if patient_object.abha_number: return False - else: - # Create abha number flow - abha_object = AbhaNumber() - abha_object.abha_number = data["healthIdNumber"] - abha_object.email = data["email"] - abha_object.first_name = data["firstName"] - abha_object.health_id = data["healthId"] - abha_object.last_name = data["lastName"] - abha_object.middle_name = data["middleName"] - abha_object.profile_photo = data["profilePhoto"] - abha_object.save() - - patient_obj.abha_number = abha_object - patient_obj.save() + + patient_object.abha_number = abha_object + patient_object.save() return True @swagger_auto_schema( @@ -140,27 +146,10 @@ def create_health_id(self, request): data = request.data serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGateway().create_health_id(data) - abha_object = AbhaNumber.objects.filter( - abha_number=response["healthIdNumber"] - ).first() - if abha_object: - # Flow when abha number exists in db somehow! - pass - else: - # Create abha number flow - abha_object = AbhaNumber() - abha_object.abha_number = response["healthIdNumber"] - abha_object.email = response["email"] - abha_object.first_name = response["firstName"] - abha_object.health_id = response["healthId"] - abha_object.last_name = response["lastName"] - abha_object.middle_name = response["middleName"] - abha_object.profile_photo = response["profilePhoto"] - abha_object.txn_id = response["healthIdNumber"] - abha_object.access_token = response["token"] - abha_object.refresh_token = data["txnId"] - abha_object.save() + abha_profile = HealthIdGateway().create_health_id(data) + + # have a serializer to verify data of abha_profile + abha_object = self.create_abha(abha_profile) if "patientId" in data: patient_id = data.pop("patientId") @@ -168,10 +157,20 @@ def create_health_id(self, request): patient_obj = allowed_patients.filter(external_id=patient_id).first() if not patient_obj: raise ValidationError({"patient": "Not Found"}) - patient_obj.abha_number = abha_object - patient_obj.save() - return Response(response, status=status.HTTP_200_OK) + 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 @@ -219,7 +218,10 @@ def confirm_with_aadhaar_otp(self, request): serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_aadhaar_otp(data) - abha_object = HealthIdGateway().get_profile(response) + abha_profile = HealthIdGateway().get_profile(response) + + # have a serializer to verify data of abha_profile + abha_object = self.create_abha(abha_profile) if "patientId" in data: patient_id = data.pop("patientId") @@ -233,11 +235,14 @@ def confirm_with_aadhaar_otp(self, request): patient_obj, ): return Response( - {"message": "ABHA NUmber / Health ID already Exists"}, + {"message": "Failed to add abha Number to the patient"}, status=status.HTTP_400_BAD_REQUEST, ) - return Response(abha_object, status=status.HTTP_200_OK) + return Response( + {"id": abha_object.external_id, "abha_profile": abha_profile}, + status=status.HTTP_200_OK, + ) # /v1/auth/confirmWithMobileOtp @swagger_auto_schema( @@ -252,7 +257,10 @@ def confirm_with_mobile_otp(self, request): serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_mobile_otp(data) - abha_object = HealthIdGateway().get_profile(response) + abha_profile = HealthIdGateway().get_profile(response) + + # have a serializer to verify data of abha_profile + abha_object = self.create_abha(abha_profile) if "patientId" in data: patient_id = data.pop("patientId") @@ -266,11 +274,14 @@ def confirm_with_mobile_otp(self, request): patient_obj, ): return Response( - {"message": "ABHA NUmber / Health ID already Exists"}, + {"message": "Failed to add abha Number to the patient"}, status=status.HTTP_400_BAD_REQUEST, ) - return Response(abha_object, status=status.HTTP_200_OK) + return Response( + {"id": abha_object.external_id, "abha_profile": abha_profile}, + status=status.HTTP_200_OK, + ) @swagger_auto_schema( operation_id="confirm_with_demographics", @@ -286,24 +297,6 @@ def confirm_with_demographics(self, request): response = HealthIdGateway().confirm_with_demographics(data) return Response(response, status=status.HTTP_200_OK) - # patient_id = data.pop("patientId") - # if patient_id and response.status: - # 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 self.add_abha_details_to_patient( - # abha_object, - # patient_obj, - # ): - # return Response(abha_object, status=status.HTTP_200_OK) - # else: - # return Response( - # {"message": "ABHA NUmber / Health ID already Exists"}, - # status=status.HTTP_400_BAD_REQUEST, - # ) - ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( From d64f4ee199a024f565d5f76274e6221ca5094d1d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Feb 2023 00:18:36 +0530 Subject: [PATCH 027/137] added extra condition while linking abha with patient --- care/abdm/api/viewsets/healthid.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 50a496ee19..d3e13c19f2 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -20,6 +20,7 @@ ) from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import HealthIdGateway, HealthIdGatewayV2 +from care.facility.models.patient import PatientRegistration from care.utils.queryset.patient import get_patient_queryset @@ -127,7 +128,11 @@ def create_abha(self, abha_profile): return abha_object def add_abha_details_to_patient(self, abha_object, patient_object): - if patient_object.abha_number: + patient = PatientRegistration.objects.filter( + abha_number__abha_number=abha_object.abha_number + ).first() + + if patient or patient_object.abha_number: return False patient_object.abha_number = abha_object From 77ba47923264c2586870ba8f30f2938a81888ed1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Feb 2023 19:10:48 +0530 Subject: [PATCH 028/137] added v0.5 apis for demographic auth --- care/abdm/api/serializers/healthid.py | 34 +++++++ care/abdm/api/viewsets/auth.py | 43 +++++++++ care/abdm/api/viewsets/healthid.py | 62 ++++++++++++- care/abdm/api/viewsets/hip.py | 45 +++++---- care/abdm/utils/api_call.py | 129 +++++++++++++++++++++++++- config/urls.py | 16 ++++ 6 files changed, 301 insertions(+), 28 deletions(-) create mode 100644 care/abdm/api/viewsets/auth.py diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 4701170711..25b157208e 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -30,6 +30,40 @@ class HealthIdSerializer(Serializer): ) +class QRContentSerializer(Serializer): + hidn = CharField( + max_length=17, + min_length=17, + required=True, + help_text="Health ID Number", + ) + phr = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Health ID", + ) + name = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Name", + ) + gender = CharField( + max_length=1, + min_length=1, + required=True, + help_text="Name", + ) + dob = CharField( + max_length=10, + min_length=8, + required=True, + help_text="Name", + ) + # {"statelgd":"33","distlgd":"573","address":"C/O Gopalsamy NO 33 A WESTSTREET ODANILAI KASTHURIBAI GRAMAM ARACHALUR Erode","state name":"TAMIL NADU","dist name":"Erode","mobile":"7639899448"} + + # { # "authMethod": "AADHAAR_OTP", # "healthid": "43-4221-5105-6749" diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py new file mode 100644 index 0000000000..7f81d8ae36 --- /dev/null +++ b/care/abdm/api/viewsets/auth.py @@ -0,0 +1,43 @@ +from rest_framework import status +from rest_framework.generics import GenericAPIView +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + +from care.abdm.utils.api_call import AbdmGateway + + +class OnFetchView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print("on-fetch-modes", data) + AbdmGateway().init(data["resp"]["requestId"]) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class OnInitView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print("on-init", data) + AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class OnConfirmView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + AbdmGateway().link_patient_abha( + data["auth"]["patient"], + data["auth"]["accessToken"], + data["resp"]["requestId"], + ) + return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index d3e13c19f2..319e8b5530 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -1,5 +1,7 @@ # ABDM HealthID APIs +from datetime import datetime + from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.decorators import action @@ -15,11 +17,13 @@ GenerateMobileOtpRequestPayloadSerializer, HealthIdAuthSerializer, HealthIdSerializer, + QRContentSerializer, VerifyDemographicsRequestPayloadSerializer, VerifyOtpRequestPayloadSerializer, ) from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import HealthIdGateway, HealthIdGatewayV2 +from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway +from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration from care.utils.queryset.patient import get_patient_queryset @@ -41,7 +45,7 @@ def generate_aadhaar_otp(self, request): data = request.data serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGatewayV2().generate_aadhaar_otp(data) + response = HealthIdGateway().generate_aadhaar_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( @@ -194,6 +198,60 @@ def search_by_health_id(self, request): response = HealthIdGateway().search_by_health_id(data) return Response(response, status=status.HTTP_200_OK) + @swagger_auto_schema( + # /v1/registration/aadhaar/searchByHealthId + operation_id="link_via_qr", + request_body=HealthIdSerializer, + responses={"200": "{'status': 'boolean'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def link_via_qr(self, request): + data = request.data + serializer = QRContentSerializer(data=data) + serializer.is_valid(raise_exception=True) + + if "patientId" not in data: + patient = PatientRegistration.objects.create( + facility=Facility.objects.get(external_id=data["facilityId"]), + name=data["name"], + gender=1 + if data["gender"] == "M" + else 2 + if data["gender"] == "F" + else 3, + is_antenatal=False, + phone_number=data["mobile"], + emergency_phone_number=data["mobile"], + date_of_birth=datetime.strptime(data["dob"], "%d-%m-%Y").date(), + blood_group="UNK", + nationality="India", + address=data["address"], + pincode=None, + created_by=None, + state=None, + district=None, + local_body=None, + ward=None, + ) + patient.save() + + patient_id = patient.external_id + else: + patient_id = data["patientId"] + + 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], + "patientId": patient_id, + } + ) + + return Response({}, status=status.HTTP_200_OK) + # auth/init @swagger_auto_schema( # /v1/auth/init diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 424eb4dcbc..05e4b6c42e 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -8,7 +8,6 @@ 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 @@ -18,27 +17,8 @@ class HipViewSet(GenericViewSet): permission_classes = (AllowAny,) authentication_classes = [] - def add_abha_details_to_patient(self, data, patient_obj): - abha_object = AbhaNumber.objects.filter( - abha_number=data["healthIdNumber"] - ).first() - if abha_object: - # Flow when abha number exists in db somehow! - pass - else: - # Create abha number flow - abha_object = AbhaNumber() - abha_object.abha_number = data["healthIdNumber"] - # abha_object.email = data["email"] - # abha_object.first_name = data["firstName"] - abha_object.health_id = data["healthId"] - # abha_object.last_name = data["lastName"] - # abha_object.middle_name = data["middleName"] - # abha_object.profile_photo = data["profilePhoto"] - abha_object.save() - - patient_obj.abha_number = abha_object - patient_obj.save() + def add_abha_details_to_patient(self, data): + AbdmGateway().fetch_modes(data) return True def demographics_verification(self, data): @@ -63,7 +43,9 @@ def share(self, request, *args, **kwargs): data = request.data patient_data = data["profile"]["patient"] - counter_id = data["profile"]["hipCode"] + counter_id = ( + "8be5ab36-1b66-44ca-ae77-c719e084160d" or data["profile"]["hipCode"] + ) patient_data["mobile"] = "" for identifier in patient_data["identifiers"]: @@ -104,7 +86,22 @@ def share(self, request, *args, **kwargs): local_body=None, ward=None, ) - self.add_abha_details_to_patient(patient_data, patient) + patient.save() + self.add_abha_details_to_patient( + { + "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], + "patientId": patient.external_id, + } + ) payload = { "requestId": str(uuid.uuid4()), diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 9780473773..bcaf936742 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -1,5 +1,7 @@ import json +import uuid from base64 import b64encode +from datetime import datetime, timezone import requests from Crypto.Cipher import PKCS1_v1_5 @@ -7,6 +9,9 @@ from django.conf import settings from django.core.cache import cache +from care.abdm.models import AbhaNumber +from care.facility.models import PatientRegistration + GATEWAY_API_URL = "https://dev.abdm.gov.in/" HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" @@ -296,14 +301,134 @@ def verify_document_mobile_otp(self, data): class AbdmGateway: + # TODO: replace this with in-memory db (redis) + temp_memory = {} + hip_id = "IN3210000017" + def __init__(self): self.api = APIGateway("abdm_gateway", None) + def link_patient_abha(self, patient, access_token, request_id): + data = self.temp_memory[request_id] + + abha_object = AbhaNumber() + abha_object.health_id = patient["id"] or data["healthId"] + # abha_object.email = data["email"] + # abha_object.first_name = data["firstName"] + # abha_object.health_id = data["healthId"] + # abha_object.last_name = data["lastName"] + # abha_object.middle_name = data["middleName"] + # abha_object.profile_photo = data["profilePhoto"] + abha_object.access_token = access_token + abha_object.save() + + patient = PatientRegistration.objects.filter( + external_id=data["patientId"] + ).first() + patient.abha_number = abha_object + patient.save() + # /v0.5/users/auth/fetch-modes def fetch_modes(self, data): path = "/v0.5/users/auth/fetch-modes" - response = self.api.post(path, data) - return response.json() + additional_headers = {"X-CM-ID": "sbx"} + request_id = str(uuid.uuid4()) + + """ + data = { + healthId, + name, + gender, + dateOfBirth, + patientId + } + """ + self.temp_memory[request_id] = data + + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "query": { + "id": data["healthId"], + "purpose": "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): + path = "/v0.5/users/auth/init" + additional_headers = {"X-CM-ID": "sbx"} + + request_id = str(uuid.uuid4()) + + data = self.temp_memory[prev_request_id] + self.temp_memory[request_id] = data + + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "query": { + "id": data["healthId"], + "purpose": "KYC_AND_LINK", + "authMode": "DEMOGRAPHICS", + "requester": {"type": "HIP", "id": self.hip_id}, + }, + } + response = self.api.post(path, payload, None, additional_headers) + return response + + """ + { + "requestId": guidv4, + "timestamp": isotime, + "transactionId": "xxxxxxxxxxxxxxx from on-init", + "credential": { + "demographic": { + "name": "Khavin", + "gender": "M", + "dateOfBirth": "1999-01-18" + }, + "authCode": "" + } + } + """ + # "/v0.5/users/auth/confirm" + def confirm(self, transaction_id, prev_request_id): + path = "/v0.5/users/auth/confirm" + additional_headers = {"X-CM-ID": "sbx"} + + request_id = str(uuid.uuid4()) + + data = self.temp_memory[prev_request_id] + self.temp_memory[request_id] = data + + payload = { + "requestId": request_id, + "timestamp": str( + 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": "", + }, + } + + print(payload) + + response = self.api.post(path, payload, None, additional_headers) + return response # /v1.0/patients/profile/on-share def on_share(self, data): diff --git a/config/urls.py b/config/urls.py index 9fbfc17816..b33c324130 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,6 +9,7 @@ from rest_framework import permissions from rest_framework_simplejwt.views import TokenVerifyView +from care.abdm.api.viewsets.auth import OnConfirmView, OnFetchView, OnInitView from care.facility.api.viewsets.open_id import OpenIdConfigView from care.users.api.viewsets.change_password import ChangePasswordView from care.users.reset_password_views import ( @@ -82,6 +83,21 @@ ), path("api/v1/", include(api_router.urlpatterns)), path("v1.0/patients/", include(api_router.abdm_urlpatterns)), + 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", + ), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), From 86fcdf86b4ec3cc3536bcfd47929472ae5cc6d96 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 09:19:27 +0530 Subject: [PATCH 029/137] update flow for share profile --- care/abdm/api/viewsets/hip.py | 52 +++++++++++-------- .../migrations/0006_auto_20230208_0915.py | 48 +++++++++++++++++ care/abdm/models.py | 25 ++++++--- care/abdm/utils/api_call.py | 46 ++++++++++------ 4 files changed, 126 insertions(+), 45 deletions(-) create mode 100644 care/abdm/migrations/0006_auto_20230208_0915.py diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 05e4b6c42e..e0cdd5cdd9 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -8,6 +8,7 @@ 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 @@ -17,27 +18,10 @@ class HipViewSet(GenericViewSet): permission_classes = (AllowAny,) authentication_classes = [] - def add_abha_details_to_patient(self, data): + def get_linking_token(self, data): AbdmGateway().fetch_modes(data) return True - def demographics_verification(self, data): - auth_init_response = HealthIdGateway().auth_init( - {"authMethod": "DEMOGRAPHICS", "healthid": data["healthIdNumber"]} - ) - if "txnId" in auth_init_response: - demographics_response = HealthIdGateway().confirm_with_demographics( - { - "txnId": auth_init_response["txnId"], - "name": data["name"], - "gender": data["gender"], - "yearOfBirth": data["yearOfBirth"], - } - ) - return "status" in demographics_response and demographics_response["status"] - else: - return False - @action(detail=False, methods=["POST"]) def share(self, request, *args, **kwargs): data = request.data @@ -55,7 +39,12 @@ def share(self, request, *args, **kwargs): serializer = HipShareProfileSerializer(data=data) serializer.is_valid(raise_exception=True) - if self.demographics_verification(patient_data): + 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"] ) @@ -86,8 +75,29 @@ def share(self, request, *args, **kwargs): local_body=None, ward=None, ) + + 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.add_abha_details_to_patient( + + self.get_linking_token( { "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], @@ -99,7 +109,6 @@ def share(self, request, *args, **kwargs): "%Y-%m-%d", ) )[0:10], - "patientId": patient.external_id, } ) @@ -122,7 +131,6 @@ def share(self, request, *args, **kwargs): on_share_response = AbdmGateway().on_share(payload) if on_share_response.status_code == 202: - print("on_share_header", on_share_response.request.body) return Response( on_share_response.request.body, status=status.HTTP_202_ACCEPTED, diff --git a/care/abdm/migrations/0006_auto_20230208_0915.py b/care/abdm/migrations/0006_auto_20230208_0915.py new file mode 100644 index 0000000000..ed00f037cd --- /dev/null +++ b/care/abdm/migrations/0006_auto_20230208_0915.py @@ -0,0 +1,48 @@ +# Generated by Django 2.2.11 on 2023-02-08 03:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("abdm", "0005_auto_20221220_2327"), + ] + + operations = [ + migrations.AddField( + model_name="abhanumber", + name="address", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="date_of_birth", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="district", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="gender", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="name", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="pincode", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="state", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/care/abdm/models.py b/care/abdm/models.py index d931cc98c6..18e4c0add7 100644 --- a/care/abdm/models.py +++ b/care/abdm/models.py @@ -9,15 +9,28 @@ class AbhaNumber(BaseModel): abha_number = models.TextField(null=True, blank=True) - email = models.EmailField(null=True, blank=True) - first_name = models.TextField(null=True, blank=True) health_id = models.TextField(null=True, blank=True) - last_name = 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) - profile_photo = models.TextField(null=True, blank=True) # What is profile photo? how is it stored as? - txn_id = models.TextField(null=True, blank=True) # 50? + 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 + ) # What is profile photo? how is it stored as? - access_token = models.TextField(null=True, blank=True) # 50 seems a bit too low for access tokens + 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): diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index bcaf936742..0875d7cd60 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -8,9 +8,9 @@ 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 import PatientRegistration GATEWAY_API_URL = "https://dev.abdm.gov.in/" HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" @@ -235,6 +235,23 @@ def confirm_with_demographics(self, data): 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: @@ -308,25 +325,20 @@ class AbdmGateway: def __init__(self): self.api = APIGateway("abdm_gateway", None) - def link_patient_abha(self, patient, access_token, request_id): + def save_linking_token(self, patient, access_token, request_id): data = self.temp_memory[request_id] + health_id = patient["id"] or data["healthId"] - abha_object = AbhaNumber() - abha_object.health_id = patient["id"] or data["healthId"] - # abha_object.email = data["email"] - # abha_object.first_name = data["firstName"] - # abha_object.health_id = data["healthId"] - # abha_object.last_name = data["lastName"] - # abha_object.middle_name = data["middleName"] - # abha_object.profile_photo = data["profilePhoto"] - abha_object.access_token = access_token - abha_object.save() - - patient = PatientRegistration.objects.filter( - external_id=data["patientId"] + abha_object = AbhaNumber.objects.filter( + Q(abha_number=health_id) | Q(health_id=health_id) ).first() - patient.abha_number = abha_object - patient.save() + + 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): From ecdef7e045018978640b40b5b290f6b850c266d6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 09:39:01 +0530 Subject: [PATCH 030/137] updated the flow of link_via_qr --- care/abdm/api/viewsets/healthid.py | 49 +++++++++++++++++++++++++++--- care/abdm/api/viewsets/hip.py | 2 +- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 319e8b5530..cd20139377 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -212,6 +212,26 @@ def link_via_qr(self, request): serializer.is_valid(raise_exception=True) if "patientId" not in data: + 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, + ) + + if ( + "facilityId" not in data + or not Facility.objects.filter(external_id=data["facilityId"]).first() + ): + return Response( + {"message": "Enter a valid facilityId"}, + status=status.HTTP_400_BAD_REQUEST, + ) + patient = PatientRegistration.objects.create( facility=Facility.objects.get(external_id=data["facilityId"]), name=data["name"], @@ -234,11 +254,31 @@ def link_via_qr(self, request): local_body=None, ward=None, ) - patient.save() - - patient_id = patient.external_id else: - patient_id = data["patientId"] + 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, + ) + + abha_number = AbhaNumber.objects.create( + abha_number=data["hidn"], + health_id=data["phr"], + name=data["name"], + gender=data["gender"], + date_of_birth=str(datetime.strptime(data["dob"], "%d-%m-%Y"))[0:10], + address=data["address"], + district=data["dist name"], + state=data["state name"], + ) + + abha_number.save() + patient.abha_number = abha_number + patient.save() AbdmGateway().fetch_modes( { @@ -246,7 +286,6 @@ def link_via_qr(self, request): "name": data["name"], "gender": data["gender"], "dateOfBirth": str(datetime.strptime(data["dob"], "%d-%m-%Y"))[0:10], - "patientId": patient_id, } ) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index e0cdd5cdd9..12b2c5d922 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -47,7 +47,7 @@ def share(self, request, *args, **kwargs): ): patient = PatientRegistration.objects.filter( abha_number__abha_number=patient_data["healthIdNumber"] - ) + ).first() if not patient: patient = PatientRegistration.objects.create( From b6306f7518f925014b860b0d8de1267e25cf3aec Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 09:57:23 +0530 Subject: [PATCH 031/137] verify demographics before patient creation --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/api/viewsets/healthid.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7f81d8ae36..b7c2596f35 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -35,7 +35,7 @@ class OnConfirmView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data print(data) - AbdmGateway().link_patient_abha( + AbdmGateway().save_linking_token( data["auth"]["patient"], data["auth"]["accessToken"], data["resp"]["requestId"], diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index cd20139377..57ea1673f9 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -232,6 +232,18 @@ def link_via_qr(self, request): status=status.HTTP_400_BAD_REQUEST, ) + dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() + if not HealthIdGateway().verify_demographics( + data["phr"] or data["hdin"], + data["name"], + data["gender"], + str(dob.year), + ): + return Response( + {"message": "Please enter valid data"}, + status=status.HTTP_403_FORBIDDEN, + ) + patient = PatientRegistration.objects.create( facility=Facility.objects.get(external_id=data["facilityId"]), name=data["name"], @@ -243,7 +255,7 @@ def link_via_qr(self, request): is_antenatal=False, phone_number=data["mobile"], emergency_phone_number=data["mobile"], - date_of_birth=datetime.strptime(data["dob"], "%d-%m-%Y").date(), + date_of_birth=dob, blood_group="UNK", nationality="India", address=data["address"], @@ -270,7 +282,7 @@ def link_via_qr(self, request): health_id=data["phr"], name=data["name"], gender=data["gender"], - date_of_birth=str(datetime.strptime(data["dob"], "%d-%m-%Y"))[0:10], + date_of_birth=str(dob)[0:10], address=data["address"], district=data["dist name"], state=data["state name"], @@ -289,7 +301,7 @@ def link_via_qr(self, request): } ) - return Response({}, status=status.HTTP_200_OK) + return Response({"message": "success"}, status=status.HTTP_200_OK) # auth/init @swagger_auto_schema( From 526fe218408de879dae6af6b2a5a30089126871d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 10:51:15 +0530 Subject: [PATCH 032/137] linking token while confirming with otps --- care/abdm/api/viewsets/healthid.py | 62 +++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 57ea1673f9..6550de47af 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -110,7 +110,7 @@ def verify_mobile_otp(self, request): response = HealthIdGateway().verify_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) - def create_abha(self, abha_profile): + def create_abha(self, abha_profile, token): abha_object = AbhaNumber.objects.filter( abha_number=abha_profile["healthIdNumber"] ).first() @@ -118,15 +118,30 @@ def create_abha(self, abha_profile): if abha_object: return abha_object - abha_object = AbhaNumber() - abha_object.abha_number = abha_profile["healthIdNumber"] - abha_object.email = abha_profile["email"] - abha_object.first_name = abha_profile["firstName"] - abha_object.health_id = abha_profile["healthId"] - abha_object.last_name = abha_profile["lastName"] - abha_object.middle_name = abha_profile["middleName"] - abha_object.profile_photo = abha_profile["profilePhoto"] - abha_object.txn_id = abha_profile["healthIdNumber"] + 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"], + 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 @@ -158,7 +173,14 @@ def create_health_id(self, request): abha_profile = HealthIdGateway().create_health_id(data) # have a serializer to verify data of abha_profile - abha_object = self.create_abha(abha_profile) + abha_object = self.create_abha( + abha_profile, + { + "txn_id": data["txnId"], + "access_token": abha_profile["token"], + "refresh_token": None, + }, + ) if "patientId" in data: patient_id = data.pop("patientId") @@ -335,7 +357,14 @@ def confirm_with_aadhaar_otp(self, request): abha_profile = HealthIdGateway().get_profile(response) # have a serializer to verify data of abha_profile - abha_object = self.create_abha(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") @@ -374,7 +403,14 @@ def confirm_with_mobile_otp(self, request): abha_profile = HealthIdGateway().get_profile(response) # have a serializer to verify data of abha_profile - abha_object = self.create_abha(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") From ded150ed4e0b24adfb5529b6411f9a78ba8d0d1b Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 11:06:26 +0530 Subject: [PATCH 033/137] fixed abha creation error in healthid --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 6550de47af..a978eb2f74 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -118,7 +118,7 @@ def create_abha(self, abha_profile, token): if abha_object: return abha_object - abha_object = AbhaNumber().objects.create( + abha_object = AbhaNumber.objects.create( abha_number=abha_profile["healthIdNumber"], health_id=abha_profile["healthId"], name=abha_profile["name"], From 226d5c1a550a246f0f9b675cf5f4eed60a1f1add Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 11:57:36 +0530 Subject: [PATCH 034/137] fixed address key missing issue --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index a978eb2f74..3341729371 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -132,7 +132,7 @@ def create_abha(self, abha_profile, token): "%Y-%m-%d", ) )[0:10], - address=abha_profile["address"], + address=abha_profile["address"] if "address" in abha_profile else "", district=abha_profile["districtName"], state=abha_profile["stateName"], pincode=abha_profile["pincode"], From 8e0f8498d72feb0a9fa7c00acfeb06f09861973d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 12:18:30 +0530 Subject: [PATCH 035/137] removed facility id hardcoding in profile share --- care/abdm/api/viewsets/hip.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 12b2c5d922..09945866f9 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -28,7 +28,9 @@ def share(self, request, *args, **kwargs): patient_data = data["profile"]["patient"] counter_id = ( - "8be5ab36-1b66-44ca-ae77-c719e084160d" or data["profile"]["hipCode"] + data["profile"]["hipCode"] + if len(data["profile"]["hipCode"]) == 36 + else Facility.objects.first().external_id ) patient_data["mobile"] = "" From 5143a862c0aa2b2e0ac0b505e7771ca6c3b25db9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 12:48:31 +0530 Subject: [PATCH 036/137] added pycryptodome --- requirements/base.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements/base.txt b/requirements/base.txt index da425f6ba1..057c1fe19c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -62,3 +62,5 @@ typed-ast==1.5.0 # In Memory Database # ------------------------------------------------------------------------------ littletable==2.0.7 +pycryptodome==3.16.0 +pycryptodomex==3.16.0 From 323cbc4df3f252c32d5a5feaf1081621e6dab1f1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 27 Feb 2023 13:05:01 +0530 Subject: [PATCH 037/137] minor changes --- care/abdm/utils/api_call.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 0875d7cd60..434d3d93d7 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -219,7 +219,7 @@ def auth_init(self, data): # /v1/auth/confirmWithAadhaarOtp def confirm_with_aadhaar_otp(self, data): - path = "/v1/auth/confirmWithAadhaarOtp" + path = "/v1/auth/confirmWithAadhaarOTP" response = self.api.post(path, data) return response.json() @@ -343,7 +343,7 @@ def save_linking_token(self, patient, access_token, request_id): # /v0.5/users/auth/fetch-modes def fetch_modes(self, data): path = "/v0.5/users/auth/fetch-modes" - additional_headers = {"X-CM-ID": "sbx"} + additional_headers = {"X-CM-ID": settings.X_CM_ID} request_id = str(uuid.uuid4()) """ @@ -374,7 +374,7 @@ def fetch_modes(self, data): # "/v0.5/users/auth/init" def init(self, prev_request_id): path = "/v0.5/users/auth/init" - additional_headers = {"X-CM-ID": "sbx"} + additional_headers = {"X-CM-ID": settings.X_CM_ID} request_id = str(uuid.uuid4()) @@ -396,25 +396,10 @@ def init(self, prev_request_id): response = self.api.post(path, payload, None, additional_headers) return response - """ - { - "requestId": guidv4, - "timestamp": isotime, - "transactionId": "xxxxxxxxxxxxxxx from on-init", - "credential": { - "demographic": { - "name": "Khavin", - "gender": "M", - "dateOfBirth": "1999-01-18" - }, - "authCode": "" - } - } - """ # "/v0.5/users/auth/confirm" def confirm(self, transaction_id, prev_request_id): path = "/v0.5/users/auth/confirm" - additional_headers = {"X-CM-ID": "sbx"} + additional_headers = {"X-CM-ID": settings.X_CM_ID} request_id = str(uuid.uuid4()) @@ -445,6 +430,6 @@ def confirm(self, transaction_id, prev_request_id): # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" - additional_headers = {"X-CM-ID": "sbx"} + additional_headers = {"X-CM-ID": settings.X_CM_ID} response = self.api.post(path, data, None, additional_headers) return response From d73681193f55c74dbfe498f90a17d6a1cd2929f0 Mon Sep 17 00:00:00 2001 From: Mathew Date: Mon, 27 Feb 2023 15:29:58 +0530 Subject: [PATCH 038/137] Create deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/deployment-branch.yaml diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml new file mode 100644 index 0000000000..eff2d34813 --- /dev/null +++ b/.github/workflows/deployment-branch.yaml @@ -0,0 +1,61 @@ +name: Branch based deploy + +on: + workflow_dispatch: + +env: + IMAGE_NAME: care-${{ github.ref_name}} +jobs: + + build-image: + name: Build & Push Staging to container registries + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/${{ github.repository }} + tags: | + type=raw,value=latest-${{ github.run_number }} + type=raw,value=latest + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + flavor: | + latest=true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ hashFiles('r*/base.txt', 'r*/production.txt', 'Dockerfile') }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build image + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new + + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache From 2a877a0d6cf0fa525c71129ba427732a90a8dea8 Mon Sep 17 00:00:00 2001 From: Mathew Alex Date: Mon, 27 Feb 2023 16:21:30 +0530 Subject: [PATCH 039/137] udpated image tag --- .github/workflows/deployment-branch.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index eff2d34813..3b1368df66 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -3,8 +3,6 @@ name: Branch based deploy on: workflow_dispatch: -env: - IMAGE_NAME: care-${{ github.ref_name}} jobs: build-image: @@ -20,12 +18,8 @@ jobs: images: | ghcr.io/${{ github.repository }} tags: | - type=raw,value=latest-${{ github.run_number }} - type=raw,value=latest - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=true + 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 From 131063202f95882419f0a311eaacfcbb740dc68a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 27 Feb 2023 16:23:54 +0530 Subject: [PATCH 040/137] fixed dob used before declaration error in link_via_qr --- care/abdm/api/viewsets/healthid.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 3341729371..326be9cb51 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -230,9 +230,12 @@ def search_by_health_id(self, request): @action(detail=False, methods=["post"]) def link_via_qr(self, request): data = request.data + serializer = QRContentSerializer(data=data) serializer.is_valid(raise_exception=True) + dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() + if "patientId" not in data: patient = PatientRegistration.objects.filter( abha_number__abha_number=data["hidn"] @@ -254,7 +257,6 @@ def link_via_qr(self, request): status=status.HTTP_400_BAD_REQUEST, ) - dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() if not HealthIdGateway().verify_demographics( data["phr"] or data["hdin"], data["name"], From 87b89f105d40ebae9d7b618fdc6ff952b8685f87 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 27 Feb 2023 17:32:21 +0530 Subject: [PATCH 041/137] added cm_id env --- config/settings/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/settings/base.py b/config/settings/base.py index 705ab9d694..9f3db9e747 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -269,7 +269,7 @@ "formatters": { "verbose": { "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" + "%(process)d %(thread)d %(message)s" } }, "handlers": { @@ -512,3 +512,4 @@ def GETKEY(group, request): ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") +X_CM_ID = env("X_CM_ID", default="sbx") From ce03be075dfc9cc7811a43284e0509795177d8a0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 27 Feb 2023 17:51:09 +0530 Subject: [PATCH 042/137] return patient as response for link_via_qr --- care/abdm/api/viewsets/healthid.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 326be9cb51..b7470b531e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -23,6 +23,7 @@ ) 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.facility import Facility from care.facility.models.patient import PatientRegistration from care.utils.queryset.patient import get_patient_queryset @@ -325,7 +326,8 @@ def link_via_qr(self, request): } ) - return Response({"message": "success"}, status=status.HTTP_200_OK) + patient_serialized = PatientDetailSerializer(patient).data + return Response(patient_serialized, status=status.HTTP_200_OK) # auth/init @swagger_auto_schema( From 3130220eaa12d0f49a1a5ae43d1feaa2856a1b39 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 3 Mar 2023 12:39:52 +0530 Subject: [PATCH 043/137] added authentication to abdm apis --- care/abdm/api/viewsets/abha.py | 2 ++ care/abdm/api/viewsets/healthid.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index d6eba3fe8e..ac1957a82d 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -1,5 +1,6 @@ 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 @@ -13,6 +14,7 @@ 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) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index b7470b531e..0ce25fa822 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -7,6 +7,7 @@ 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 @@ -33,6 +34,7 @@ class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): base_name = "healthid" model = AbhaNumber + permission_classes = (IsAuthenticated,) # TODO: Ratelimiting for all endpoints that generate OTP's / Critical API's @swagger_auto_schema( From d0bdf95354db73353e28020bb929843edda945bf Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Fri, 3 Mar 2023 12:51:52 +0530 Subject: [PATCH 044/137] Add Basic Auth Ratelimiting on all Endpoints --- config/authentication.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/config/authentication.py b/config/authentication.py index 0d104efc78..a2dd9d69ef 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -2,9 +2,11 @@ import jwt import requests +from django.contrib.auth import authenticate from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from rest_framework import HTTP_HEADER_ENCODING +from rest_framework import HTTP_HEADER_ENCODING, exceptions +from rest_framework import status from rest_framework.authentication import BasicAuthentication from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken @@ -12,6 +14,7 @@ from care.facility.models import Facility from care.facility.models.asset import Asset from care.users.models import User +from config.ratelimit import ratelimit class CustomJWTAuthentication(JWTAuthentication): @@ -31,6 +34,32 @@ def get_validated_token(self, raw_token): class CustomBasicAuthentication(BasicAuthentication): + + def authenticate_credentials(self, userid, password, request=None): + """ + Authenticate the userid and password against username and password + with optional request for context. + """ + from config.auth_views import CaptchaRequiredException + credentials = { + User.USERNAME_FIELD: userid, + 'password': password + } + if ratelimit(request, "login", [userid]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + user = authenticate(request=request, **credentials) + + if user is None: + raise exceptions.AuthenticationFailed(_('Invalid username/password.')) + + if not user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + + return (user, None) + def authenticate_header(self, request): return "" From 1155a8c3b7e7a31943343baa72e9af3e5951a4c2 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Fri, 3 Mar 2023 13:08:57 +0530 Subject: [PATCH 045/137] Fix Ratelimting bug --- config/authentication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/authentication.py b/config/authentication.py index a2dd9d69ef..7953c4526e 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -45,7 +45,7 @@ def authenticate_credentials(self, userid, password, request=None): User.USERNAME_FIELD: userid, 'password': password } - if ratelimit(request, "login", [userid]): + if ratelimit(request, "login", [userid], increment=False): raise CaptchaRequiredException( detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, code=status.HTTP_429_TOO_MANY_REQUESTS, @@ -53,6 +53,7 @@ def authenticate_credentials(self, userid, password, request=None): user = authenticate(request=request, **credentials) if user is None: + ratelimit(request, "login", [userid]) raise exceptions.AuthenticationFailed(_('Invalid username/password.')) if not user.is_active: From 007688babdff7131f7af71c76660804101531233 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 3 Mar 2023 15:22:48 +0530 Subject: [PATCH 046/137] added rate limit for abdm apis --- care/abdm/api/viewsets/healthid.py | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 0ce25fa822..8ad029865b 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -28,6 +28,8 @@ from care.facility.models.facility import Facility from care.facility.models.patient import 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 @@ -46,6 +48,13 @@ class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): @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) @@ -61,6 +70,13 @@ def generate_aadhaar_otp(self, request): @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) @@ -76,6 +92,13 @@ def resend_aadhaar_otp(self, request): @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( @@ -93,6 +116,13 @@ def verify_aadhaar_otp(self, request): @action(detail=False, methods=["post"]) def generate_mobile_otp(self, request): data = request.data + + if ratelimit(request, "generate_mobile_otp", [data["mobile"], 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) @@ -108,6 +138,13 @@ def generate_mobile_otp(self, request): @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) @@ -171,6 +208,13 @@ def add_abha_details_to_patient(self, abha_object, patient_object): @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) @@ -218,6 +262,13 @@ def create_health_id(self, request): @action(detail=False, methods=["post"]) def search_by_health_id(self, request): data = request.data + + if ratelimit(request, "search_by_health_id", [data["healthId"]]): + 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) @@ -342,6 +393,13 @@ def link_via_qr(self, request): @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) @@ -357,6 +415,13 @@ def auth_init(self, request): @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) @@ -403,6 +468,13 @@ def confirm_with_aadhaar_otp(self, request): @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) @@ -448,6 +520,13 @@ def confirm_with_mobile_otp(self, request): @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) @@ -465,6 +544,13 @@ def confirm_with_demographics(self, request): @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) From 31f17d44d75df4752bf117e463dc4af607a56455 Mon Sep 17 00:00:00 2001 From: Mathew Date: Sat, 4 Mar 2023 00:14:45 +0530 Subject: [PATCH 047/137] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a08bc74922..9508259583 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Care Backend +

From 9c76263e3201bbf79857944e34425bac71c94926 Mon Sep 17 00:00:00 2001 From: Mathew Date: Sat, 4 Mar 2023 00:15:37 +0530 Subject: [PATCH 048/137] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 3b1368df66..6ede8a37f7 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -2,7 +2,12 @@ name: Branch based deploy on: workflow_dispatch: - + + push: + branches: + - abdm-m1-vignesh + paths-ignore: + - "docs/**" jobs: build-image: From c8e7a126c2d942f2fd6d5dfaef58ca315d7290f6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Mar 2023 17:06:40 +0530 Subject: [PATCH 049/137] fixed ratelimiting for generate_mobile_otp --- care/abdm/api/viewsets/healthid.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 8ad029865b..3d66336ba8 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -117,7 +117,7 @@ def verify_aadhaar_otp(self, request): def generate_mobile_otp(self, request): data = request.data - if ratelimit(request, "generate_mobile_otp", [data["mobile"], data["txnId"]]): + 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, @@ -263,7 +263,9 @@ def create_health_id(self, request): def search_by_health_id(self, request): data = request.data - if ratelimit(request, "search_by_health_id", [data["healthId"]]): + 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, From 2649f9c2193b6b625179840c3ec46676df3842ac Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 6 Mar 2023 18:54:28 +0530 Subject: [PATCH 050/137] Add Ratelimiting Docs comment --- config/ratelimit.py | 1 + config/settings/base.py | 1 + 2 files changed, 2 insertions(+) diff --git a/config/ratelimit.py b/config/ratelimit.py index 4d59c8dcf3..435076c726 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -21,6 +21,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 9f3db9e747..e5e1976226 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -324,6 +324,7 @@ def GETKEY(group, request): return "ratelimit" +# https://django-ratelimit.readthedocs.io/en/stable/rates.html DJANGO_RATE_LIMIT = env("RATE_LIMIT", default="5/10m") GOOGLE_RECAPTCHA_SECRET_KEY = env("GOOGLE_RECAPTCHA_SECRET_KEY", default="") From 50332f993f4eac263c7f66f06161d1422cb0920b Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Mar 2023 17:22:31 +0530 Subject: [PATCH 051/137] allow null for abha_number while registering patient --- care/facility/api/serializers/patient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 2852b0cbe7..16373a0a9e 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -182,7 +182,7 @@ class Meta: ) abha_number = ExternalIdSerializerField( - queryset=AbhaNumber.objects.all(), required=False + queryset=AbhaNumber.objects.all(), required=False, allow_null=True ) abha_number_object = AbhaNumberSerializer(source="abha_number", read_only=True) From 4605dc3f9a55f5236ada639735ddd48439dd8b86 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Mar 2023 17:42:05 +0530 Subject: [PATCH 052/137] fixed confirm_with_aadhaar_otp path --- care/abdm/utils/api_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 434d3d93d7..5359637ef1 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -219,7 +219,7 @@ def auth_init(self, data): # /v1/auth/confirmWithAadhaarOtp def confirm_with_aadhaar_otp(self, data): - path = "/v1/auth/confirmWithAadhaarOTP" + path = "/v1/auth/confirmWithAadhaarOtp" response = self.api.post(path, data) return response.json() From 3ba753ae587c146a8399f5db246a1b0a4633cec9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Mar 2023 17:58:24 +0530 Subject: [PATCH 053/137] removed existing abha id validation --- care/abdm/api/viewsets/healthid.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 3d66336ba8..a7d77fd1ad 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -187,12 +187,12 @@ def create_abha(self, abha_profile, token): return abha_object def add_abha_details_to_patient(self, abha_object, patient_object): - patient = PatientRegistration.objects.filter( - abha_number__abha_number=abha_object.abha_number - ).first() + # patient = PatientRegistration.objects.filter( + # abha_number__abha_number=abha_object.abha_number + # ).first() - if patient or patient_object.abha_number: - return False + # if patient or patient_object.abha_number: + # return False patient_object.abha_number = abha_object patient_object.save() From c63b2bedbcb422e2d8a11341c7bfb124d2559bbf Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 30 Mar 2023 10:03:44 +0530 Subject: [PATCH 054/137] added care-context linking flow --- care/abdm/api/viewsets/auth.py | 130 +++++++++++++++++++++++++++++ care/abdm/api/viewsets/healthid.py | 24 ++++++ care/abdm/api/viewsets/hip.py | 26 +++++- care/abdm/utils/api_call.py | 125 ++++++++++++++++++++++++++- config/urls.py | 30 ++++++- 5 files changed, 332 insertions(+), 3 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index b7c2596f35..3a7a0fb957 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -4,6 +4,8 @@ from rest_framework.response import Response from care.abdm.utils.api_call import AbdmGateway +from care.facility.models.patient import PatientRegistration +from care.facility.models.patient_consultation import PatientConsultation class OnFetchView(GenericAPIView): @@ -41,3 +43,131 @@ def post(self, request, *args, **kwargs): data["resp"]["requestId"], ) return Response({}, status=status.HTTP_202_ACCEPTED) + + +class OnAddContextsView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class DiscoverView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + 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["type"] == "MOBILE": + matched_by.append(identifier["value"]) + patients = patients.filter(phone_number=identifier["value"]) + + 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"] + ) + + patients.filter( + abha_number__name=data["patient"]["name"], + abha_number__gender=data["patient"]["gender"], + # TODO: check date also + ) + + if len(patients) != 1: + 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(patients[0].external_id), + "patient_name": patients[0].name, + "care_contexts": list( + map( + lambda consultation: { + "id": str(consultation.external_id), + "name": f"Encounter: {str(consultation.created_date.date())}", + }, + PatientConsultation.objects.filter(patient=patients[0]), + ) + ), + "matched_by": matched_by, + } + ) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class LinkInitView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + 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 = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + + # TODO: verify otp + + patient = PatientRegistration.objects.get( + 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) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index a7d77fd1ad..553de6278e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -384,6 +384,30 @@ def link_via_qr(self, request): patient_serialized = PatientDetailSerializer(patient).data return Response(patient_serialized, status=status.HTTP_200_OK) + @swagger_auto_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 + + 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) + # auth/init @swagger_auto_schema( # /v1/auth/init diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 09945866f9..c218d657f0 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -11,7 +11,7 @@ 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 care.facility.models.patient import PatientConsultation, PatientRegistration class HipViewSet(GenericViewSet): @@ -145,3 +145,27 @@ def share(self, request, *args, **kwargs): }, status=status.HTTP_401_UNAUTHORIZED, ) + + # TODO: move it somewhere appropriate + @action(detail=False, methods=["POST"]) + def add_care_context(self, request, *args, **kwargs): + consultation_id = request.data["consultation"] + + consultation = PatientConsultation.objects.get(external_id=consultation_id) + + if not consultation: + return Response( + {"consultation": "No matching records found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + response = AbdmGateway().add_contexts( + { + "access_token": consultation.patient.abha_number.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(response) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 5359637ef1..6fa0af658e 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -1,7 +1,7 @@ import json import uuid from base64 import b64encode -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import requests from Crypto.Cipher import PKCS1_v1_5 @@ -427,6 +427,129 @@ def confirm(self, transaction_id, prev_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": str( + 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": str( + 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": str( + 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": str( + 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 + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/config/urls.py b/config/urls.py index b33c324130..efadb8bbd7 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,7 +9,15 @@ from rest_framework import permissions from rest_framework_simplejwt.views import TokenVerifyView -from care.abdm.api.viewsets.auth import OnConfirmView, OnFetchView, OnInitView +from care.abdm.api.viewsets.auth import ( + DiscoverView, + LinkConfirmView, + LinkInitView, + OnAddContextsView, + OnConfirmView, + OnFetchView, + OnInitView, +) from care.facility.api.viewsets.open_id import OpenIdConfigView from care.users.api.viewsets.change_password import ChangePasswordView from care.users.reset_password_views import ( @@ -98,6 +106,26 @@ OnConfirmView.as_view(), name="abdm_on_confirm_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", + ), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), From 70e437b20c6680d1e2dfeb0b3d0653dca152666a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 30 Mar 2023 11:40:24 +0530 Subject: [PATCH 055/137] added consent flow --- care/abdm/api/viewsets/auth.py | 59 ++++++++++++++++++++++++++++++++++ care/abdm/utils/api_call.py | 18 +++++++++++ config/urls.py | 6 ++++ 3 files changed, 83 insertions(+) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 3a7a0fb957..10162adbe4 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -1,3 +1,6 @@ +import json + +from django.core.cache import cache from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny @@ -171,3 +174,59 @@ def post(self, request, *args, **kwargs): ) return Response({}, status=status.HTTP_202_ACCEPTED) + + +class NotifyView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + + # TODO: create a seperate cache and also add a expiration time + cache.set(data["notification"]["consentId"], json.dumps(data)) + + # data = { + # "requestId": "5f7a535d-a3fd-416b-b069-c97d021fbacd", + # "timestamp": "2023-03-30T05:00:31.288Z", + # "notification": { + # "status": "GRANTED", + # "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + # "consentDetail": { + # "schemaVersion": "string", + # "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + # "createdAt": "2023-03-30T05:00:31.288Z", + # "patient": {"id": "hinapatel79@ndhm"}, + # "careContexts": [ + # { + # "patientReference": "hinapatel79@hospital", + # "careContextReference": "Episode1", + # } + # ], + # "purpose": {"text": "string", "code": "string", "refUri": "string"}, + # "hip": {"id": "string", "name": "TESI-HIP"}, + # "consentManager": {"id": "string"}, + # "hiTypes": ["OPConsultation"], + # "permission": { + # "accessMode": "VIEW", + # "dateRange": { + # "from": "2023-03-30T05:00:31.288Z", + # "to": "2023-03-30T05:00:31.288Z", + # }, + # "dataEraseAt": "2023-03-30T05:00:31.288Z", + # "frequency": {"unit": "HOUR", "value": 0, "repeats": 0}, + # }, + # }, + # "signature": "Signature of CM as defined in W3C standards; Base64 encoded", + # "grantAcknowledgement": False, + # }, + # } + + AbdmGateway().on_notify( + { + "request_id": data["requestId"], + "consent_id": data["notification"]["consentId"], + } + ) + return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 6fa0af658e..e6f7ef4937 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -550,6 +550,24 @@ def on_link_confirm(self, data): 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": str( + 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 + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/config/urls.py b/config/urls.py index efadb8bbd7..64c58c67e4 100644 --- a/config/urls.py +++ b/config/urls.py @@ -13,6 +13,7 @@ DiscoverView, LinkConfirmView, LinkInitView, + NotifyView, OnAddContextsView, OnConfirmView, OnFetchView, @@ -126,6 +127,11 @@ LinkConfirmView.as_view(), name="abdm_link_confirm_view", ), + path( + "v0.5/consents/hip/notify", + NotifyView.as_view(), + name="abdm_notify_view", + ), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), From 859891f778a5806c780ad094ca83176bd46adc0e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 31 Mar 2023 10:59:26 +0530 Subject: [PATCH 056/137] added data transfer flow --- care/abdm/api/viewsets/auth.py | 156 +++++++++++++++++++++++++++++++++ care/abdm/utils/api_call.py | 81 +++++++++++++++++ care/abdm/utils/fhir.py | 23 +++++ config/urls.py | 6 ++ requirements/base.txt | 1 + 5 files changed, 267 insertions(+) create mode 100644 care/abdm/utils/fhir.py diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 10162adbe4..5b3031da59 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -1,12 +1,16 @@ import json +from datetime import datetime, timezone +import nacl.utils from django.core.cache import cache +from nacl.public import Box, PrivateKey from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from care.abdm.utils.api_call import AbdmGateway +from care.abdm.utils.fhir import create_consultation_bundle from care.facility.models.patient import PatientRegistration from care.facility.models.patient_consultation import PatientConsultation @@ -230,3 +234,155 @@ def post(self, request, *args, **kwargs): } ) return Response({}, status=status.HTTP_202_ACCEPTED) + + +class RequestDataView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + + consent_id = data["hiRequest"]["consent"]["id"] + consent = cache[consent_id] if consent_id not in cache else None + if not consent or not consent["notification"]["status"] == "GRANTED": + return Response({}, status=status.HTTP_401_UNAUTHORIZED) + + 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) + + AbdmGateway.on_data_request( + {"request_id": data["requestId"], "transaction_id": data["transactionId"]} + ) + + secret_key = PrivateKey.generate() + public_key = secret_key.public_key.encode().hex() + hiu_nonce = data["hiRequest"]["keyMaterial"]["nonce"].replace("-", "") + nonce = nacl.utils.random(32).hex() + xor_nonce = hex(int(hiu_nonce, base=16) ^ int(nonce, base=16))[2:] + + shared_key = Box( + secret_key, data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"] + ) + + AbdmGateway.data_transfer( + { + "transaction_id": data["transactionId"], + "data_push_url": data["hiRequest"]["dataPushUrl"], + "care_contexts": list( + map( + lambda context: { + "patient_id": context["patientReference"], + "consultation_id": context["careContextReference"], + "data": shared_key.encrypt( + create_consultation_bundle( + PatientConsultation.objects.get( + external_id=context["consultation_id"] + ) + ), + xor_nonce, + ), + }, + consent["notification"]["consentDetail"]["careContexts"], + ) + ), + "key_material": { + "cryptoAlg": "ECDH", + "curve": "Curve25519", + "dhPublicKey": { + "expiry": str( + datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ) + ), # not sure what to put here + "parameters": f"Curve25519/{public_key}", # not sure what to put here + "keyValue": public_key, + }, + "nonce": 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"], + ) + ), + } + ) + + return Response({}, status=status.HTTP_202_ACCEPTED) + + +# consent = { +# "requestId": "5f7a535d-a3fd-416b-b069-c97d021fbacd", +# "timestamp": "2023-03-30T05:00:31.288Z", +# "notification": { +# "status": "GRANTED", +# "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +# "consentDetail": { +# "schemaVersion": "string", +# "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +# "createdAt": "2023-03-30T05:00:31.288Z", +# "patient": {"id": "hinapatel79@ndhm"}, +# "careContexts": [ +# { +# "patientReference": "hinapatel79@hospital", +# "careContextReference": "Episode1", +# } +# ], +# "purpose": {"text": "string", "code": "string", "refUri": "string"}, +# "hip": {"id": "string", "name": "TESI-HIP"}, +# "consentManager": {"id": "string"}, +# "hiTypes": ["OPConsultation"], +# "permission": { +# "accessMode": "VIEW", +# "dateRange": { +# "from": "2023-03-30T05:00:31.288Z", +# "to": "2023-03-30T05:00:31.288Z", +# }, +# "dataEraseAt": "2023-03-30T05:00:31.288Z", +# "frequency": {"unit": "HOUR", "value": 0, "repeats": 0}, +# }, +# }, +# "signature": "Signature of CM as defined in W3C standards; Base64 encoded", +# "grantAcknowledgement": False, +# }, +# } + +# data = { +# "requestId": "a1s2c932-2f70-3ds3-a3b5-2sfd46b12a18d", +# "timestamp": "2023-03-30T06:37:05.476Z", +# "transactionId": "a1s2c932-2f70-3ds3-a3b5-2sfd46b12a18d", +# "hiRequest": { +# "consent": {"id": "string"}, +# "dateRange": { +# "from": "2023-03-30T06:37:05.476Z", +# "to": "2023-03-30T06:37:05.476Z", +# }, +# "dataPushUrl": "string", +# "keyMaterial": { +# "cryptoAlg": "ECDH", +# "curve": "Curve25519", +# "dhPublicKey": { +# "expiry": "2023-03-30T06:37:05.476Z", +# "parameters": "Curve25519/32byte random key", +# "keyValue": "string", +# }, +# "nonce": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +# }, +# }, +# } diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index e6f7ef4937..b4ab714ff7 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -568,6 +568,87 @@ def on_notify(self, data): 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": str( + 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 = {"Authorization": f"Bearer {cache.get(ABDM_TOKEN_CACHE_KEY)}"} + + payload = { + "pageNumber": 0, + "pageCount": 0, + "transactionId": data["transaction_id"], + "entries": list( + map( + lambda context: { + "content": context["data"], + "media": "application/fhir+json", + "checksum": "string", + "careContextReference": context["consultation_id"], + }, + ) + ), + "keyMaterial": data["key_material"], + } + + response = requests.post(data["data_push_url"], 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": str( + 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") + ), + "notifier": {"type": "HIP", "id": self.hip_id}, + "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 + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py new file mode 100644 index 0000000000..6924c537ff --- /dev/null +++ b/care/abdm/utils/fhir.py @@ -0,0 +1,23 @@ +from fhir.resources.bundle import Bundle, BundleEntry +from fhir.resources.domainresource import DomainResource +from fhir.resources.encounter import Encounter + + +def get_reference_url(self, resource: DomainResource): + return f"{resource.resource_type}/{resource.id}" + + +def create_encounter(consultation): + return Encounter( + id=consultation.external_id, + status="discharged" if consultation.discharge_date else "in-progress", + ) + + +def create_consultation_bundle(consultation): + encounter = create_encounter(consultation) + + return Bundle( + id=consultation.patient.external_id, + entry=[BundleEntry(fullUrl=get_reference_url(encounter), resource=encounter)], + ) diff --git a/config/urls.py b/config/urls.py index 64c58c67e4..5353e33294 100644 --- a/config/urls.py +++ b/config/urls.py @@ -18,6 +18,7 @@ OnConfirmView, OnFetchView, OnInitView, + RequestDataView, ) from care.facility.api.viewsets.open_id import OpenIdConfigView from care.users.api.viewsets.change_password import ChangePasswordView @@ -132,6 +133,11 @@ NotifyView.as_view(), name="abdm_notify_view", ), + path( + "v0.5/health-information/hip/request", + RequestDataView.as_view(), + name="abdm_request_data_view", + ), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), diff --git a/requirements/base.txt b/requirements/base.txt index 057c1fe19c..d7ae7b383a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -64,3 +64,4 @@ typed-ast==1.5.0 littletable==2.0.7 pycryptodome==3.16.0 pycryptodomex==3.16.0 +fhir.resources==6.5.0 From 918d7d1c7994be6b730ff2f5752d9026261b1c73 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 3 Apr 2023 12:59:44 +0530 Subject: [PATCH 057/137] changed encryption according to abdm docs --- care/abdm/api/viewsets/auth.py | 193 ++++++++++++++++----------------- care/abdm/utils/api_call.py | 4 + care/abdm/utils/fhir.py | 13 ++- 3 files changed, 108 insertions(+), 102 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 5b3031da59..d43c850229 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -1,9 +1,14 @@ +import base64 import json from datetime import datetime, timezone import nacl.utils +from Crypto.Cipher import AES +from Crypto.Hash import SHA512 +from Crypto.Protocol.KDF import HKDF from django.core.cache import cache -from nacl.public import Box, PrivateKey +from nacl.encoding import Base64Encoder +from nacl.public import Box, PrivateKey, PublicKey from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny @@ -189,43 +194,7 @@ def post(self, request, *args, **kwargs): print(data) # TODO: create a seperate cache and also add a expiration time - cache.set(data["notification"]["consentId"], json.dumps(data)) - - # data = { - # "requestId": "5f7a535d-a3fd-416b-b069-c97d021fbacd", - # "timestamp": "2023-03-30T05:00:31.288Z", - # "notification": { - # "status": "GRANTED", - # "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - # "consentDetail": { - # "schemaVersion": "string", - # "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - # "createdAt": "2023-03-30T05:00:31.288Z", - # "patient": {"id": "hinapatel79@ndhm"}, - # "careContexts": [ - # { - # "patientReference": "hinapatel79@hospital", - # "careContextReference": "Episode1", - # } - # ], - # "purpose": {"text": "string", "code": "string", "refUri": "string"}, - # "hip": {"id": "string", "name": "TESI-HIP"}, - # "consentManager": {"id": "string"}, - # "hiTypes": ["OPConsultation"], - # "permission": { - # "accessMode": "VIEW", - # "dateRange": { - # "from": "2023-03-30T05:00:31.288Z", - # "to": "2023-03-30T05:00:31.288Z", - # }, - # "dataEraseAt": "2023-03-30T05:00:31.288Z", - # "frequency": {"unit": "HOUR", "value": 0, "repeats": 0}, - # }, - # }, - # "signature": "Signature of CM as defined in W3C standards; Base64 encoded", - # "grantAcknowledgement": False, - # }, - # } + cache.set(data["notification"]["consentDetail"]["consentId"], json.dumps(data)) AbdmGateway().on_notify( { @@ -244,36 +213,50 @@ def post(self, request, *args, **kwargs): data = request.data print(data) + # TODO: uncomment later consent_id = data["hiRequest"]["consent"]["id"] - consent = cache[consent_id] if consent_id not in cache else None + 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) - 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) - - AbdmGateway.on_data_request( + # 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) + + AbdmGateway().on_data_request( {"request_id": data["requestId"], "transaction_id": data["transactionId"]} ) + hiu_public_key_b64 = data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"] + hiu_public_key_hex = base64.b64decode(hiu_public_key_b64).hex()[2:] + hiu_public_key_hex_x = hiu_public_key_hex[:64] + # hiu_public_key_hex_y = hiu_public_key_hex[64:] + hiu_public_key = PublicKey(bytes.fromhex(hiu_public_key_hex_x)) + hiu_nonce = data["hiRequest"]["keyMaterial"]["nonce"] + secret_key = PrivateKey.generate() - public_key = secret_key.public_key.encode().hex() - hiu_nonce = data["hiRequest"]["keyMaterial"]["nonce"].replace("-", "") + public_key = secret_key.public_key.encode(Base64Encoder) nonce = nacl.utils.random(32).hex() - xor_nonce = hex(int(hiu_nonce, base=16) ^ int(nonce, base=16))[2:] - shared_key = Box( - secret_key, data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"] - ) + xored_nonce = hex( + int(base64.b64decode(hiu_nonce).hex(), base=16) ^ int(nonce, base=16) + )[2:] + salt = xored_nonce[:40] + iv = xored_nonce[40:] + shared_key = Box(secret_key, hiu_public_key).encode(Base64Encoder).hex() + + hkdf_key = HKDF(bytes.fromhex(shared_key), 32, bytes.fromhex(salt), SHA512) + + cipher = AES.new(hkdf_key, AES.MODE_GCM, iv.encode("utf8")) - AbdmGateway.data_transfer( + AbdmGateway().data_transfer( { "transaction_id": data["transactionId"], "data_push_url": data["hiRequest"]["dataPushUrl"], @@ -282,16 +265,17 @@ def post(self, request, *args, **kwargs): lambda context: { "patient_id": context["patientReference"], "consultation_id": context["careContextReference"], - "data": shared_key.encrypt( + "data": cipher.encrypt( create_consultation_bundle( PatientConsultation.objects.get( - external_id=context["consultation_id"] + external_id=context["careContextReference"] ) - ), - xor_nonce, + ) + .json() + .encode("utf8") ), }, - consent["notification"]["consentDetail"]["careContexts"], + consent["notification"]["consentDetail"]["careContexts"][3:], ) ), "key_material": { @@ -328,61 +312,74 @@ def post(self, request, *args, **kwargs): # consent = { -# "requestId": "5f7a535d-a3fd-416b-b069-c97d021fbacd", -# "timestamp": "2023-03-30T05:00:31.288Z", # "notification": { -# "status": "GRANTED", -# "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", # "consentDetail": { -# "schemaVersion": "string", -# "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", -# "createdAt": "2023-03-30T05:00:31.288Z", -# "patient": {"id": "hinapatel79@ndhm"}, -# "careContexts": [ -# { -# "patientReference": "hinapatel79@hospital", -# "careContextReference": "Episode1", -# } -# ], -# "purpose": {"text": "string", "code": "string", "refUri": "string"}, -# "hip": {"id": "string", "name": "TESI-HIP"}, -# "consentManager": {"id": "string"}, +# "consentId": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218", +# "createdAt": "2023-03-31T15:30:58.212283603", +# "purpose": {"text": "Self Requested", "code": "PATRQT", "refUri": None}, +# "patient": {"id": "khavinshankar@sbx"}, +# "consentManager": {"id": "sbx"}, +# "hip": {"id": "IN3210000017", "name": "Coronasafe Care 01"}, # "hiTypes": ["OPConsultation"], # "permission": { # "accessMode": "VIEW", # "dateRange": { -# "from": "2023-03-30T05:00:31.288Z", -# "to": "2023-03-30T05:00:31.288Z", +# "from": "2023-03-29T15:28:00", +# "to": "2023-03-31T15:28:00", # }, -# "dataEraseAt": "2023-03-30T05:00:31.288Z", -# "frequency": {"unit": "HOUR", "value": 0, "repeats": 0}, +# "dataEraseAt": "2023-04-01T15:28:18.501", +# "frequency": {"unit": "HOUR", "value": 1, "repeats": 0}, # }, +# "careContexts": [ +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "c7134ba2-692a-40f5-a143-d306896436dd", +# }, +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "56015494-bac8-486d-85b6-6f67d1708764", +# }, +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", +# }, +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", +# }, +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", +# }, +# ], # }, -# "signature": "Signature of CM as defined in W3C standards; Base64 encoded", +# "status": "GRANTED", +# "signature": "jnG9oxlV6jRfxqXgW781ehe5/VYyzG5Z3aWNsgMJB2GB4IGKRu6ZqMu82WYJOKJqY62Oy7J90XBPAWWacQJVoa1eq1qcw6Fgc7pejihVVN/Ohdu2S6LSIi27DdVRQLR//7bCTSfe1P+3qCj+GkMVGgX0LbtYp2n3awZ0kRZFDt5JUI1oqWItx4Zz8pOF+1zjhD+AdzydE4JrKl3o/qICsb6+C9Iqe0ZrfqWAOmpESD17Z0p6trzkbHgeWXW/7S4Fg27cAJt9Z+HCa4PZLTOm5yx231QXyTRKCPrSQsZDe/OR5fUu3b0bDWf4F1FIJKXLG8ZmlsCs0T1gs3n8MkWYmQ==", +# "consentId": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218", # "grantAcknowledgement": False, # }, +# "requestId": "99b5e499-c81f-42f9-a550-e0eef2b1e2c1", +# "timestamp": "2023-03-31T15:30:58.236624856", # } + # data = { -# "requestId": "a1s2c932-2f70-3ds3-a3b5-2sfd46b12a18d", -# "timestamp": "2023-03-30T06:37:05.476Z", -# "transactionId": "a1s2c932-2f70-3ds3-a3b5-2sfd46b12a18d", +# "transactionId": "2839dccc-c9e5-4e29-8904-440a1dc7f0cf", +# "requestId": "87e509d3-c43e-4da5-a39c-296c01740a79", +# "timestamp": "2023-03-31T15:31:28.587999924", # "hiRequest": { -# "consent": {"id": "string"}, -# "dateRange": { -# "from": "2023-03-30T06:37:05.476Z", -# "to": "2023-03-30T06:37:05.476Z", -# }, -# "dataPushUrl": "string", +# "consent": {"id": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218"}, +# "dateRange": {"from": "2023-03-29T15:28:00", "to": "2023-03-31T15:28:00"}, +# "dataPushUrl": "https://dev.abdm.gov.in/api-hiu/data/notification", # "keyMaterial": { # "cryptoAlg": "ECDH", -# "curve": "Curve25519", +# "curve": "curve25519", # "dhPublicKey": { -# "expiry": "2023-03-30T06:37:05.476Z", -# "parameters": "Curve25519/32byte random key", -# "keyValue": "string", +# "expiry": "2023-04-02T15:30:58.49682", +# "parameters": "Ephemeral public key", +# "keyValue": "BHkJo9SpkcGmxTNqo4pYdvGuZ/ELbwwCxoLbqyY5kuSyJ42FBfQUsLkg8prSQrzk5lIwQ3JEuXYsignQT5juGow=", # }, -# "nonce": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +# "nonce": "EAeHOfrH6xNXxj2nM6TClwJ6k7FNWQ9UzAx2ylVyCzE=", # }, # }, # } diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index b4ab714ff7..ae4c746c19 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -604,12 +604,16 @@ def data_transfer(self, data): "checksum": "string", "careContextReference": context["consultation_id"], }, + data["care_contexts"], ) ), "keyMaterial": data["key_material"], } response = requests.post(data["data_push_url"], payload, headers=headers) + print("-----------------------------------------") + print("data response", response.text, response.status_code) + print("-----------------------------------------") return response def data_notify(self, data): diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 6924c537ff..296038ae1c 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,16 +1,20 @@ from fhir.resources.bundle import Bundle, BundleEntry +from fhir.resources.coding import Coding from fhir.resources.domainresource import DomainResource from fhir.resources.encounter import Encounter -def get_reference_url(self, resource: DomainResource): +def get_reference_url(resource: DomainResource): return f"{resource.resource_type}/{resource.id}" def create_encounter(consultation): return Encounter( - id=consultation.external_id, - status="discharged" if consultation.discharge_date else "in-progress", + **{ + "id": str(consultation.external_id), + "status": "discharged" if consultation.discharge_date else "in-progress", + "class": Coding(code="IMP", display="Inpatient Encounter"), + } ) @@ -18,6 +22,7 @@ def create_consultation_bundle(consultation): encounter = create_encounter(consultation) return Bundle( - id=consultation.patient.external_id, + id=str(consultation.patient.external_id), + type="collection", entry=[BundleEntry(fullUrl=get_reference_url(encounter), resource=encounter)], ) From b8baf9ea3b6d1dabb7b4fdd2117b6b22f65cc802 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 6 Apr 2023 09:25:55 +0530 Subject: [PATCH 058/137] data transfer flow (working version with hardcoded data) --- care/abdm/api/viewsets/auth.py | 138 +++++------------------------ care/abdm/utils/api_call.py | 19 ++-- care/abdm/utils/cipher.py | 65 ++++++++++++++ care/abdm/utils/fhir.py | 154 ++++++++++++++++++++++++++++++++- 4 files changed, 252 insertions(+), 124 deletions(-) create mode 100644 care/abdm/utils/cipher.py diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index d43c850229..d3f02649e9 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -1,20 +1,14 @@ -import base64 import json -from datetime import datetime, timezone +from datetime import datetime, timedelta -import nacl.utils -from Crypto.Cipher import AES -from Crypto.Hash import SHA512 -from Crypto.Protocol.KDF import HKDF from django.core.cache import cache -from nacl.encoding import Base64Encoder -from nacl.public import Box, PrivateKey, PublicKey from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from care.abdm.utils.api_call import AbdmGateway +from care.abdm.utils.cipher import Cipher from care.abdm.utils.fhir import create_consultation_bundle from care.facility.models.patient import PatientRegistration from care.facility.models.patient_consultation import PatientConsultation @@ -230,31 +224,21 @@ def post(self, request, *args, **kwargs): # if not consent_from < now and now > consent_to: # return Response({}, status=status.HTTP_403_FORBIDDEN) - AbdmGateway().on_data_request( + on_data_request_response = AbdmGateway().on_data_request( {"request_id": data["requestId"], "transaction_id": data["transactionId"]} ) - hiu_public_key_b64 = data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"] - hiu_public_key_hex = base64.b64decode(hiu_public_key_b64).hex()[2:] - hiu_public_key_hex_x = hiu_public_key_hex[:64] - # hiu_public_key_hex_y = hiu_public_key_hex[64:] - hiu_public_key = PublicKey(bytes.fromhex(hiu_public_key_hex_x)) - hiu_nonce = data["hiRequest"]["keyMaterial"]["nonce"] - - secret_key = PrivateKey.generate() - public_key = secret_key.public_key.encode(Base64Encoder) - nonce = nacl.utils.random(32).hex() - - xored_nonce = hex( - int(base64.b64decode(hiu_nonce).hex(), base=16) ^ int(nonce, base=16) - )[2:] - salt = xored_nonce[:40] - iv = xored_nonce[40:] - shared_key = Box(secret_key, hiu_public_key).encode(Base64Encoder).hex() + if not on_data_request_response.status_code == 202: + return Response( + on_data_request_response, status=status.HTTP_400_BAD_REQUEST + ) - hkdf_key = HKDF(bytes.fromhex(shared_key), 32, bytes.fromhex(salt), SHA512) + cipher = Cipher( + data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"], + data["hiRequest"]["keyMaterial"]["nonce"], + ) - cipher = AES.new(hkdf_key, AES.MODE_GCM, iv.encode("utf8")) + print(consent["notification"]["consentDetail"]["careContexts"][:1:-1]) AbdmGateway().data_transfer( { @@ -271,26 +255,22 @@ def post(self, request, *args, **kwargs): external_id=context["careContextReference"] ) ) - .json() - .encode("utf8") - ), + )["data"], }, - consent["notification"]["consentDetail"]["careContexts"][3:], + consent["notification"]["consentDetail"]["careContexts"][ + :-4:-1 + ], ) ), "key_material": { "cryptoAlg": "ECDH", "curve": "Curve25519", "dhPublicKey": { - "expiry": str( - datetime.now(tz=timezone.utc).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ) - ), # not sure what to put here - "parameters": f"Curve25519/{public_key}", # not sure what to put here - "keyValue": public_key, + "expiry": (datetime.now() + timedelta(days=2)).isoformat(), + "parameters": "Curve25519/32byte random key", + "keyValue": cipher.key_to_share, }, - "nonce": nonce, + "nonce": cipher.sender_nonce, }, } ) @@ -302,84 +282,12 @@ def post(self, request, *args, **kwargs): "care_contexts": list( map( lambda context: {"id": context["careContextReference"]}, - consent["notification"]["consentDetail"]["careContexts"], + consent["notification"]["consentDetail"]["careContexts"][ + :-4:-1 + ], ) ), } ) return Response({}, status=status.HTTP_202_ACCEPTED) - - -# consent = { -# "notification": { -# "consentDetail": { -# "consentId": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218", -# "createdAt": "2023-03-31T15:30:58.212283603", -# "purpose": {"text": "Self Requested", "code": "PATRQT", "refUri": None}, -# "patient": {"id": "khavinshankar@sbx"}, -# "consentManager": {"id": "sbx"}, -# "hip": {"id": "IN3210000017", "name": "Coronasafe Care 01"}, -# "hiTypes": ["OPConsultation"], -# "permission": { -# "accessMode": "VIEW", -# "dateRange": { -# "from": "2023-03-29T15:28:00", -# "to": "2023-03-31T15:28:00", -# }, -# "dataEraseAt": "2023-04-01T15:28:18.501", -# "frequency": {"unit": "HOUR", "value": 1, "repeats": 0}, -# }, -# "careContexts": [ -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "c7134ba2-692a-40f5-a143-d306896436dd", -# }, -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "56015494-bac8-486d-85b6-6f67d1708764", -# }, -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", -# }, -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", -# }, -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", -# }, -# ], -# }, -# "status": "GRANTED", -# "signature": "jnG9oxlV6jRfxqXgW781ehe5/VYyzG5Z3aWNsgMJB2GB4IGKRu6ZqMu82WYJOKJqY62Oy7J90XBPAWWacQJVoa1eq1qcw6Fgc7pejihVVN/Ohdu2S6LSIi27DdVRQLR//7bCTSfe1P+3qCj+GkMVGgX0LbtYp2n3awZ0kRZFDt5JUI1oqWItx4Zz8pOF+1zjhD+AdzydE4JrKl3o/qICsb6+C9Iqe0ZrfqWAOmpESD17Z0p6trzkbHgeWXW/7S4Fg27cAJt9Z+HCa4PZLTOm5yx231QXyTRKCPrSQsZDe/OR5fUu3b0bDWf4F1FIJKXLG8ZmlsCs0T1gs3n8MkWYmQ==", -# "consentId": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218", -# "grantAcknowledgement": False, -# }, -# "requestId": "99b5e499-c81f-42f9-a550-e0eef2b1e2c1", -# "timestamp": "2023-03-31T15:30:58.236624856", -# } - - -# data = { -# "transactionId": "2839dccc-c9e5-4e29-8904-440a1dc7f0cf", -# "requestId": "87e509d3-c43e-4da5-a39c-296c01740a79", -# "timestamp": "2023-03-31T15:31:28.587999924", -# "hiRequest": { -# "consent": {"id": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218"}, -# "dateRange": {"from": "2023-03-29T15:28:00", "to": "2023-03-31T15:28:00"}, -# "dataPushUrl": "https://dev.abdm.gov.in/api-hiu/data/notification", -# "keyMaterial": { -# "cryptoAlg": "ECDH", -# "curve": "curve25519", -# "dhPublicKey": { -# "expiry": "2023-04-02T15:30:58.49682", -# "parameters": "Ephemeral public key", -# "keyValue": "BHkJo9SpkcGmxTNqo4pYdvGuZ/ELbwwCxoLbqyY5kuSyJ42FBfQUsLkg8prSQrzk5lIwQ3JEuXYsignQT5juGow=", -# }, -# "nonce": "EAeHOfrH6xNXxj2nM6TClwJ6k7FNWQ9UzAx2ylVyCzE=", -# }, -# }, -# } diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index ae4c746c19..09cfc9feb3 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -590,11 +590,11 @@ def on_data_request(self, data): return response def data_transfer(self, data): - headers = {"Authorization": f"Bearer {cache.get(ABDM_TOKEN_CACHE_KEY)}"} + headers = {"Content-Type": "application/json"} payload = { - "pageNumber": 0, - "pageCount": 0, + "pageNumber": 1, + "pageCount": 1, "transactionId": data["transaction_id"], "entries": list( map( @@ -610,9 +610,17 @@ def data_transfer(self, data): "keyMaterial": data["key_material"], } - response = requests.post(data["data_push_url"], payload, headers=headers) + response = requests.post( + data["data_push_url"], data=json.dumps(payload), headers=headers + ) print("-----------------------------------------") - print("data response", response.text, response.status_code) + print( + "data response", + len(data["care_contexts"]), + json.dumps(payload), + response.text, + response.status_code, + ) print("-----------------------------------------") return response @@ -632,7 +640,6 @@ def data_notify(self, data): "doneAt": str( datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") ), - "notifier": {"type": "HIP", "id": self.hip_id}, "statusNotification": { "sessionStatus": "TRANSFERRED", "hipId": self.hip_id, diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py new file mode 100644 index 0000000000..85e45435d0 --- /dev/null +++ b/care/abdm/utils/cipher.py @@ -0,0 +1,65 @@ +import json + +import requests + + +class Cipher: + server_url = "http://localhost:8090" + + 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 index 296038ae1c..cbf31a8b62 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,3 +1,5 @@ +import json + from fhir.resources.bundle import Bundle, BundleEntry from fhir.resources.coding import Coding from fhir.resources.domainresource import DomainResource @@ -11,7 +13,7 @@ def get_reference_url(resource: DomainResource): def create_encounter(consultation): return Encounter( **{ - "id": str(consultation.external_id), + "id": str(str(consultation.external_id)), "status": "discharged" if consultation.discharge_date else "in-progress", "class": Coding(code="IMP", display="Inpatient Encounter"), } @@ -21,8 +23,154 @@ def create_encounter(consultation): def create_consultation_bundle(consultation): encounter = create_encounter(consultation) + return json.dumps( + { + "resourceType": "Bundle", + "id": "3739707e-1123-46fe-918f-b52d880e4e7f", + "meta": {"lastUpdated": "2016-08-07T00:00:00.000+05:30"}, + "identifier": { + "system": "https://www.max.in/bundle", + "value": "3739707e-1123-46fe-918f-b52d880e4e7f", + }, + "type": "document", + "timestamp": "2016-08-07T00:00:00.000+05:30", + "entry": [ + { + "fullUrl": "Composition/c63d1435-b6b6-46c4-8163-33133bf0d9bf", + "resource": { + "resourceType": "Composition", + "id": "c63d1435-b6b6-46c4-8163-33133bf0d9bf", + "identifier": { + "system": "https://www.max.in/document", + "value": "c63d1435-b6b6-46c4-8163-33133bf0d9bf", + }, + "status": "final", + "type": { + "coding": [ + { + "system": "https://projecteka.in/sct", + "code": "440545006", + "display": "Prescription record", + } + ] + }, + "subject": { + "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" + }, + "encounter": { + "reference": f"Encounter/{str(consultation.external_id)}" + }, + "date": "2016-08-07T00:00:00.605+05:30", + "author": [ + { + "reference": "Practitioner/MAX5001", + "display": "Dr Laxmikanth J", + } + ], + "title": "Prescription", + "section": [ + { + "title": "OPD Prescription", + "code": { + "coding": [ + { + "system": "https://projecteka.in/sct", + "code": "440545006", + "display": "Prescription record", + } + ] + }, + "entry": [ + { + "reference": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0" + } + ], + } + ], + }, + }, + { + "fullUrl": "Practitioner/MAX5001", + "resource": { + "resourceType": "Practitioner", + "id": "MAX5001", + "identifier": [ + { + "system": "https://www.mciindia.in/doctor", + "value": "MAX5001", + } + ], + "name": [ + {"text": "Laxmikanth J", "prefix": ["Dr"], "suffix": ["MD"]} + ], + }, + }, + { + "fullUrl": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4", + "resource": { + "resourceType": "Patient", + "id": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "name": [{"text": "KhavinShankar G"}], + "gender": "male", + }, + }, + { + "fullUrl": f"Encounter/{str(consultation.external_id)}", + "resource": { + "resourceType": "Encounter", + "id": str(consultation.external_id), + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "AMB", + "display": "Outpatient visit", + }, + "subject": { + "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" + }, + "period": {"start": "2016-08-07T00:00:00+05:30"}, + }, + }, + { + "fullUrl": "Medication/54ab5657-5e79-4461-a823-20e522eb337d", + "resource": { + "resourceType": "Medication", + "id": "54ab5657-5e79-4461-a823-20e522eb337d", + "code": { + "coding": [ + { + "system": "https://projecteka.in/act", + "code": "R05CB02", + "display": "bromhexine 24 mg", + } + ] + }, + }, + }, + { + "fullUrl": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0", + "resource": { + "resourceType": "MedicationRequest", + "id": "68d9667c-00c3-455f-b75d-d580950498a0", + "status": "active", + "intent": "order", + "medicationReference": { + "reference": "Medication/54ab5657-5e79-4461-a823-20e522eb337d" + }, + "subject": { + "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" + }, + "authoredOn": "2016-08-07T00:00:00+05:30", + "requester": {"reference": "Practitioner/MAX5001"}, + "dosageInstruction": [{"text": "1 capsule 2 times a day"}], + }, + }, + ], + } + ) + return Bundle( id=str(consultation.patient.external_id), - type="collection", + type="document", entry=[BundleEntry(fullUrl=get_reference_url(encounter), resource=encounter)], - ) + ).json() From 82514d807ef4456ad1ab010890d0c4aad2278b10 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 6 Apr 2023 14:22:52 +0530 Subject: [PATCH 059/137] added create_prescription_record in fhir --- care/abdm/api/viewsets/auth.py | 10 +- care/abdm/utils/fhir.py | 255 ++++++++++++++++++++++++++++++--- 2 files changed, 241 insertions(+), 24 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index d3f02649e9..62d532874c 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -9,7 +9,7 @@ from care.abdm.utils.api_call import AbdmGateway from care.abdm.utils.cipher import Cipher -from care.abdm.utils.fhir import create_consultation_bundle +from care.abdm.utils.fhir import Fhir from care.facility.models.patient import PatientRegistration from care.facility.models.patient_consultation import PatientConsultation @@ -250,15 +250,15 @@ def post(self, request, *args, **kwargs): "patient_id": context["patientReference"], "consultation_id": context["careContextReference"], "data": cipher.encrypt( - create_consultation_bundle( + Fhir( PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ) + ).create_prescription_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ - :-4:-1 + :-2:-1 ], ) ), @@ -283,7 +283,7 @@ def post(self, request, *args, **kwargs): map( lambda context: {"id": context["careContextReference"]}, consent["notification"]["consentDetail"]["careContexts"][ - :-4:-1 + :-2:-1 ], ) ), diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index cbf31a8b62..62d482af6f 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,28 +1,251 @@ import json +from datetime import datetime, timezone +from uuid import uuid4 as uuid from fhir.resources.bundle import Bundle, BundleEntry +from fhir.resources.codeableconcept import CodeableConcept from fhir.resources.coding import Coding -from fhir.resources.domainresource import DomainResource +from fhir.resources.composition import Composition, CompositionSection +from fhir.resources.dosage import Dosage from fhir.resources.encounter import Encounter +from fhir.resources.humanname import HumanName +from fhir.resources.identifier import Identifier +from fhir.resources.medication import Medication +from fhir.resources.medicationrequest import MedicationRequest +from fhir.resources.meta import Meta +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.reference import Reference -def get_reference_url(resource: DomainResource): - return f"{resource.resource_type}/{resource.id}" +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._medication_profiles = [] + self._medication_request_profiles = [] -def create_encounter(consultation): - return Encounter( - **{ - "id": str(str(consultation.external_id)), - "status": "discharged" if consultation.discharge_date else "in-progress", - "class": Coding(code="IMP", display="Inpatient Encounter"), - } - ) + def _reference_url(self, resource=None): + if resource is None: + return "" + return f"{resource.resource_type}/{resource.id}" -def create_consultation_bundle(consultation): - encounter = create_encounter(consultation) + 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 + 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) + name = self.consultation.facility.name + self._organization_profile = Organization( + id=id, + identifier=[Identifier(value=id)], + name=name, + ) + + return self._organization_profile + + def _encounter(self): + 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), + } + ) + + return self._encounter_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']}" + ) + + 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 _composition(self, type): + id = str(uuid()) # TODO: use identifiable id + return Composition( + id=id, + identifier=Identifier(value=id), + status="preliminary" or "final" or "amended", # TODO: use appropriate one + type=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="440545006", + display="Prescription record", + ) + ] + ), # TODO: make it dynamic + title=type, # "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())], + ) + + pr = { + "entry": [ + {"fullUrl": "Practitioner/MAX5001", "resource": ""}, + { + "fullUrl": "Patient/RVH9999", + "resource": "", + }, + { + "fullUrl": "Encounter/dab7fd2b-6a05-4adb-af35-bcffd6c85b81", + "resource": "", + }, + { + "fullUrl": "Medication/54ab5657-5e79-4461-a823-20e522eb337d", + "resource": "", + }, + { + "fullUrl": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0", + "resource": "", + }, + ], + } + + 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() + composition_profile = self._composition("Prescription") + return Bundle( + id=id, + identifier=Identifier(value=id), + type="document", + meta=Meta(lastUpdated=now), + timestamp=now, + entry=[ + self._bundle_entry(composition_profile), + 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_consultation_bundle(consultation): return json.dumps( { "resourceType": "Bundle", @@ -168,9 +391,3 @@ def create_consultation_bundle(consultation): ], } ) - - return Bundle( - id=str(consultation.patient.external_id), - type="document", - entry=[BundleEntry(fullUrl=get_reference_url(encounter), resource=encounter)], - ).json() From 9e2574cdf087e561d4e47f9d868921eb951021af Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 6 Apr 2023 14:25:59 +0530 Subject: [PATCH 060/137] removed unwanted variables --- care/abdm/utils/fhir.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 62d482af6f..e58453bee3 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -188,28 +188,6 @@ def _composition(self, type): author=[self._reference(self._organization())], ) - pr = { - "entry": [ - {"fullUrl": "Practitioner/MAX5001", "resource": ""}, - { - "fullUrl": "Patient/RVH9999", - "resource": "", - }, - { - "fullUrl": "Encounter/dab7fd2b-6a05-4adb-af35-bcffd6c85b81", - "resource": "", - }, - { - "fullUrl": "Medication/54ab5657-5e79-4461-a823-20e522eb337d", - "resource": "", - }, - { - "fullUrl": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0", - "resource": "", - }, - ], - } - def _bundle_entry(self, resource): return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) From b3b0439ee0d62eca1d1579944394b478faa07c1c Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 10 Apr 2023 17:59:48 +0530 Subject: [PATCH 061/137] added wellness record --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/utils/api_call.py | 9 - care/abdm/utils/fhir.py | 343 ++++++++++++++++++--------------- 3 files changed, 187 insertions(+), 167 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 62d532874c..a331195e52 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -254,7 +254,7 @@ def post(self, request, *args, **kwargs): PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ).create_prescription_record() + ).create_wellness_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 09cfc9feb3..47410fe729 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -613,15 +613,6 @@ def data_transfer(self, data): response = requests.post( data["data_push_url"], data=json.dumps(payload), headers=headers ) - print("-----------------------------------------") - print( - "data response", - len(data["care_contexts"]), - json.dumps(payload), - response.text, - response.status_code, - ) - print("-----------------------------------------") return response def data_notify(self, data): diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index e58453bee3..32c60cedc3 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,11 +1,12 @@ -import json from datetime import datetime, timezone from uuid import uuid4 as uuid +from fhir.resources.address import Address from fhir.resources.bundle import Bundle, BundleEntry from fhir.resources.codeableconcept import CodeableConcept from fhir.resources.coding import Coding from fhir.resources.composition import Composition, CompositionSection +from fhir.resources.contactpoint import ContactPoint from fhir.resources.dosage import Dosage from fhir.resources.encounter import Encounter from fhir.resources.humanname import HumanName @@ -13,10 +14,12 @@ 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.quantity import Quantity from fhir.resources.reference import Reference @@ -30,6 +33,7 @@ def __init__(self, consultation): self._encounter_profile = None self._medication_profiles = [] self._medication_request_profiles = [] + self._observation_profiles = [] def _reference_url(self, resource=None): if resource is None: @@ -78,15 +82,122 @@ def _organization(self): 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(value=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 _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(' ', '')}" 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): if self._encounter_profile is not None: return self._encounter_profile @@ -124,9 +235,7 @@ def _medication_request(self, medicine): 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']}" - ) + dosage_text = f"{medicine['dosage_new']} / {medicine['dosage']} for {medicine['days']} days" medication_profile = self._medication(medicine["medicine"]) medication_request_profile = MedicationRequest( @@ -144,12 +253,12 @@ def _medication_request(self, medicine): self._medication_request_profiles.append(medication_request_profile) return medication_profile, medication_request_profile - def _composition(self, type): + def _prescription_composition(self): id = str(uuid()) # TODO: use identifiable id return Composition( id=id, identifier=Identifier(value=id), - status="preliminary" or "final" or "amended", # TODO: use appropriate one + status="final", # TODO: use appropriate one type=CodeableConcept( coding=[ Coding( @@ -158,8 +267,8 @@ def _composition(self, type): display="Prescription record", ) ] - ), # TODO: make it dynamic - title=type, # "Prescription" + ), + title="Prescription", date=datetime.now(timezone.utc).isoformat(), section=[ CompositionSection( @@ -188,13 +297,57 @@ def _composition(self, type): 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 _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() - composition_profile = self._composition("Prescription") return Bundle( id=id, identifier=Identifier(value=id), @@ -202,7 +355,7 @@ def create_prescription_record(self): meta=Meta(lastUpdated=now), timestamp=now, entry=[ - self._bundle_entry(composition_profile), + self._bundle_entry(self._prescription_composition()), self._bundle_entry(self._practioner()), self._bundle_entry(self._patient()), self._bundle_entry(self._organization()), @@ -222,150 +375,26 @@ def create_prescription_record(self): ], ).json() - -def create_consultation_bundle(consultation): - return json.dumps( - { - "resourceType": "Bundle", - "id": "3739707e-1123-46fe-918f-b52d880e4e7f", - "meta": {"lastUpdated": "2016-08-07T00:00:00.000+05:30"}, - "identifier": { - "system": "https://www.max.in/bundle", - "value": "3739707e-1123-46fe-918f-b52d880e4e7f", - }, - "type": "document", - "timestamp": "2016-08-07T00:00:00.000+05:30", - "entry": [ - { - "fullUrl": "Composition/c63d1435-b6b6-46c4-8163-33133bf0d9bf", - "resource": { - "resourceType": "Composition", - "id": "c63d1435-b6b6-46c4-8163-33133bf0d9bf", - "identifier": { - "system": "https://www.max.in/document", - "value": "c63d1435-b6b6-46c4-8163-33133bf0d9bf", - }, - "status": "final", - "type": { - "coding": [ - { - "system": "https://projecteka.in/sct", - "code": "440545006", - "display": "Prescription record", - } - ] - }, - "subject": { - "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" - }, - "encounter": { - "reference": f"Encounter/{str(consultation.external_id)}" - }, - "date": "2016-08-07T00:00:00.605+05:30", - "author": [ - { - "reference": "Practitioner/MAX5001", - "display": "Dr Laxmikanth J", - } - ], - "title": "Prescription", - "section": [ - { - "title": "OPD Prescription", - "code": { - "coding": [ - { - "system": "https://projecteka.in/sct", - "code": "440545006", - "display": "Prescription record", - } - ] - }, - "entry": [ - { - "reference": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0" - } - ], - } - ], - }, - }, - { - "fullUrl": "Practitioner/MAX5001", - "resource": { - "resourceType": "Practitioner", - "id": "MAX5001", - "identifier": [ - { - "system": "https://www.mciindia.in/doctor", - "value": "MAX5001", - } - ], - "name": [ - {"text": "Laxmikanth J", "prefix": ["Dr"], "suffix": ["MD"]} - ], - }, - }, - { - "fullUrl": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4", - "resource": { - "resourceType": "Patient", - "id": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "name": [{"text": "KhavinShankar G"}], - "gender": "male", - }, - }, - { - "fullUrl": f"Encounter/{str(consultation.external_id)}", - "resource": { - "resourceType": "Encounter", - "id": str(consultation.external_id), - "status": "finished", - "class": { - "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", - "code": "AMB", - "display": "Outpatient visit", - }, - "subject": { - "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" - }, - "period": {"start": "2016-08-07T00:00:00+05:30"}, - }, - }, - { - "fullUrl": "Medication/54ab5657-5e79-4461-a823-20e522eb337d", - "resource": { - "resourceType": "Medication", - "id": "54ab5657-5e79-4461-a823-20e522eb337d", - "code": { - "coding": [ - { - "system": "https://projecteka.in/act", - "code": "R05CB02", - "display": "bromhexine 24 mg", - } - ] - }, - }, - }, - { - "fullUrl": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0", - "resource": { - "resourceType": "MedicationRequest", - "id": "68d9667c-00c3-455f-b75d-d580950498a0", - "status": "active", - "intent": "order", - "medicationReference": { - "reference": "Medication/54ab5657-5e79-4461-a823-20e522eb337d" - }, - "subject": { - "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" - }, - "authoredOn": "2016-08-07T00:00:00+05:30", - "requester": {"reference": "Practitioner/MAX5001"}, - "dosageInstruction": [{"text": "1 capsule 2 times a day"}], - }, - }, + 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() From b953073793143ac79e4f8e106fda6c03e857d302 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 10 Apr 2023 18:49:39 +0530 Subject: [PATCH 062/137] added immunization profile --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/utils/fhir.py | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index a331195e52..79dc795644 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -254,7 +254,7 @@ def post(self, request, *args, **kwargs): PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ).create_wellness_record() + ).create_immunization_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 32c60cedc3..293378cb6e 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -11,6 +11,7 @@ from fhir.resources.encounter import Encounter 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 @@ -223,6 +224,47 @@ def _encounter(self): return self._encounter_profile + def _immunization(self): + if not self.consultation.patient.is_vaccinated: + return + + return 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 _medication(self, name): medication_profile = Medication(id=str(uuid()), code=CodeableConcept(text=name)) @@ -342,6 +384,43 @@ def _wellness_composition(self): 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())], + ), + ], + subject=self._reference(self._patient()), + encounter=self._reference(self._encounter()), + author=[self._reference(self._organization())], + ) + def _bundle_entry(self, resource): return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) @@ -398,3 +477,22 @@ def create_wellness_record(self): ), ], ).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() From 477e642acba7064b8dc88b7db19bb75c0298745a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 10 Apr 2023 22:26:17 +0530 Subject: [PATCH 063/137] added health document profile --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/utils/fhir.py | 97 +++++++++++++++++++++++++++++ care/facility/models/file_upload.py | 11 ++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 79dc795644..05b73aafd0 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -254,7 +254,7 @@ def post(self, request, *args, **kwargs): PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ).create_immunization_record() + ).create_health_document_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 293378cb6e..b0b1621422 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,12 +1,15 @@ +import base64 from datetime import datetime, timezone from uuid import uuid4 as uuid from fhir.resources.address import Address +from fhir.resources.attachment import Attachment from fhir.resources.bundle import Bundle, BundleEntry from fhir.resources.codeableconcept import CodeableConcept from fhir.resources.coding import Coding from fhir.resources.composition import Composition, CompositionSection from fhir.resources.contactpoint import ContactPoint +from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent from fhir.resources.dosage import Dosage from fhir.resources.encounter import Encounter from fhir.resources.humanname import HumanName @@ -23,6 +26,8 @@ from fhir.resources.quantity import Quantity from fhir.resources.reference import Reference +from care.facility.models.file_upload import FileUpload + class Fhir: def __init__(self, consultation): @@ -35,6 +40,7 @@ def __init__(self, consultation): self._medication_profiles = [] self._medication_request_profiles = [] self._observation_profiles = [] + self._document_reference_profiles = [] def _reference_url(self, resource=None): if resource is None: @@ -265,6 +271,27 @@ def _immunization(self): ], ) + 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)) @@ -339,6 +366,52 @@ def _prescription_composition(self): 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( @@ -496,3 +569,27 @@ def create_immunization_record(self): self._bundle_entry(self._immunization()), ], ).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() diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index b72378b0f8..21cdcb4a73 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -103,3 +103,14 @@ def read_signed_url(self): ExpiresIn=60 * 60, # One Hour ) return signed_url + + 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 From ecb674505d6916f88d121d434de0c1e32890903e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 11 Apr 2023 11:53:11 +0530 Subject: [PATCH 064/137] added discharge summary profile --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/utils/fhir.py | 298 ++++++++++++++++++++++++++++++++- 2 files changed, 297 insertions(+), 3 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 05b73aafd0..7c315b3379 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -254,7 +254,7 @@ def post(self, request, *args, **kwargs): PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ).create_health_document_record() + ).create_discharge_summary_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index b0b1621422..3e735c5f14 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -3,15 +3,18 @@ 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.documentreference import DocumentReference, DocumentReferenceContent from fhir.resources.dosage import Dosage -from fhir.resources.encounter import Encounter +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 @@ -23,10 +26,12 @@ 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.static_data.icd11 import ICDDiseases class Fhir: @@ -37,10 +42,13 @@ def __init__(self, consultation): self._practitioner_profile = None self._organization_profile = None self._encounter_profile = None + self._careplan_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: @@ -117,6 +125,94 @@ def _organization(self): 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 _observation(self, title, value, id, date): if not value or (type(value) == dict and not value["value"]): return @@ -205,7 +301,7 @@ def _observations_from_daily_round(self, daily_round): self._observation_profiles.extend(observation_profiles) return observation_profiles - def _encounter(self): + def _encounter(self, include_diagnosis=False): if self._encounter_profile is not None: return self._encounter_profile @@ -225,6 +321,26 @@ def _encounter(self): "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, } ) @@ -494,6 +610,129 @@ def _immunization_composition(self): 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 _bundle_entry(self, resource): return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) @@ -593,3 +832,58 @@ def create_health_document_record(self): ), ], ).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() From d1ea874c3667edc3b9db76698166af01c836dd79 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 11 Apr 2023 13:40:39 +0530 Subject: [PATCH 065/137] support sending multiple profiles --- care/abdm/api/viewsets/auth.py | 122 ++++++++++++++++++++++++++++----- care/abdm/utils/fhir.py | 18 +++++ 2 files changed, 123 insertions(+), 17 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7c315b3379..2f3c5c33ce 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -244,23 +244,35 @@ def post(self, request, *args, **kwargs): { "transaction_id": data["transactionId"], "data_push_url": data["hiRequest"]["dataPushUrl"], - "care_contexts": list( - map( - lambda context: { - "patient_id": context["patientReference"], - "consultation_id": context["careContextReference"], - "data": cipher.encrypt( - Fhir( - PatientConsultation.objects.get( - external_id=context["careContextReference"] - ) - ).create_discharge_summary_record() - )["data"], - }, - consent["notification"]["consentDetail"]["careContexts"][ - :-2:-1 - ], - ) + "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", @@ -291,3 +303,79 @@ def post(self, request, *args, **kwargs): ) return Response({}, status=status.HTTP_202_ACCEPTED) + + +consent = { + "notification": { + "consentDetail": { + "consentId": "0ad38ac1-5f61-480a-b9d6-6ace3e2d2139", + "createdAt": "2023-04-11T08:01:55.554799671", + "purpose": {"text": "Care Management", "code": "CAREMGT", "refUri": None}, + "patient": {"id": "khavinshankar@sbx"}, + "consentManager": {"id": "sbx"}, + "hip": {"id": "IN3210000017", "name": "Coronasafe Care 01"}, + "hiTypes": [ + "DiagnosticReport", + "DischargeSummary", + "HealthDocumentRecord", + "ImmunizationRecord", + "OPConsultation", + "Prescription", + "WellnessRecord", + ], + "permission": { + "accessMode": "VIEW", + "dateRange": { + "from": "2023-04-11T08:01:32.774", + "to": "2023-04-11T08:01:32.774", + }, + "dataEraseAt": "2023-04-12T08:01:32.774", + "frequency": {"unit": "HOUR", "value": 1, "repeats": 0}, + }, + "careContexts": [ + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "c7134ba2-692a-40f5-a143-d306896436dd", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "56015494-bac8-486d-85b6-6f67d1708764", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "66d32279-b3b3-43e1-971e-eadc5da67e95", + }, + ], + }, + "status": "GRANTED", + "signature": "oB2JZD5CMecjW3y817dYK9pqE36yE3W+jUPtc0vfrPOMEFoYftdXAnNHxBUYZ0FKKIAJGf3erLxkzx0KE+ISFJyXX4U8OzKTBGJEjjJJ7/reDRSWnXS41D89/l8kmZHtVNsqmkje4BMKAjQylw9i8js+VaVpbgC7+NtYcSfLPWqPLnw+ppFJVKM3vrL7/w1UUrrSWB27YOX02XYj4eBtjxiLneG6fTzTT7QqrUtaYYFTU7CY1Ujwx+/q82J1sz3FGszFBe4c+1orqs2jwyLSgu73qmsySdJM1ugjjWs2Y/EBG6SnWjvfz7rvfDZ0KLfcUnWfEU5FVj8umGucAshnXA==", + "consentId": "0ad38ac1-5f61-480a-b9d6-6ace3e2d2139", + "grantAcknowledgement": False, + }, + "requestId": "12fa9195-031a-4d89-b96c-0d7e3cfd009f", + "timestamp": "2023-04-11T08:01:55.570970194", +} diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 3e735c5f14..c9cde0d4ed 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -887,3 +887,21 @@ def create_discharge_summary_record(self): ), ], ).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_discharge_summary_record() + elif record_type == "DischargeSummary": + return self.create_discharge_summary_record() + elif record_type == "OPConsultation": + return self.create_discharge_summary_record() + else: + return self.create_discharge_summary_record() From b9e94a9048d8941fbf559c0f2a4022f56ead40b4 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 13 Apr 2023 11:21:07 +0530 Subject: [PATCH 066/137] merged conflicting migrations --- .../migrations/0344_merge_20230413_1120.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 care/facility/migrations/0344_merge_20230413_1120.py diff --git a/care/facility/migrations/0344_merge_20230413_1120.py b/care/facility/migrations/0344_merge_20230413_1120.py new file mode 100644 index 0000000000..96acc3ff38 --- /dev/null +++ b/care/facility/migrations/0344_merge_20230413_1120.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.11 on 2023-04-13 05:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0343_auto_20230407_1850'), + ('facility', '0331_auto_20230130_1652'), + ] + + operations = [ + ] From c0cbad9059050a6c16e7f832330fefbe342a11d0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 13 Apr 2023 13:53:24 +0530 Subject: [PATCH 067/137] integrated fidelius as a docker service --- care/abdm/utils/cipher.py | 3 ++- config/settings/base.py | 1 + docker-compose.yaml | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py index 85e45435d0..2401d1e1ed 100644 --- a/care/abdm/utils/cipher.py +++ b/care/abdm/utils/cipher.py @@ -1,10 +1,11 @@ import json import requests +from django.conf import settings class Cipher: - server_url = "http://localhost:8090" + server_url = settings.FIDELIUS_URL def __init__(self, reciever_public_key, reciever_nonce): self.reciever_public_key = reciever_public_key diff --git a/config/settings/base.py b/config/settings/base.py index 2661552dc2..da7fd307f8 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -515,3 +515,4 @@ def GETKEY(group, request): ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") X_CM_ID = env("X_CM_ID", default="sbx") +FIDELIUS_URL = env("FIDELIUS_URL", default="http://fidelius:8090") diff --git a/docker-compose.yaml b/docker-compose.yaml index bac47aa106..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: \ No newline at end of file + postgres-data: From 7dcac1db90ad99e319ac27453fc5e3fa76d02080 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 14 Apr 2023 13:56:29 +0530 Subject: [PATCH 068/137] added op consultation profile --- care/abdm/utils/fhir.py | 180 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index c9cde0d4ed..6c9b999936 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -733,6 +733,129 @@ def _discharge_summary_composition(self): 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) @@ -888,6 +1011,61 @@ def create_discharge_summary_record(self): ], ).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() @@ -902,6 +1080,6 @@ def create_record(self, record_type): elif record_type == "DischargeSummary": return self.create_discharge_summary_record() elif record_type == "OPConsultation": - return self.create_discharge_summary_record() + return self.create_op_consultation_record() else: return self.create_discharge_summary_record() From 4608e92b15156c578b437feac5688997eaac95fc Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 14 Apr 2023 17:25:50 +0530 Subject: [PATCH 069/137] added diagnostic report profile --- care/abdm/utils/fhir.py | 102 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 6c9b999936..c71d5cd811 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -12,6 +12,7 @@ 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 @@ -31,6 +32,7 @@ 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 @@ -43,6 +45,7 @@ def __init__(self, consultation): self._organization_profile = None self._encounter_profile = None self._careplan_profile = None + self._diagnostic_report_profile = None self._medication_profiles = [] self._medication_request_profiles = [] self._observation_profiles = [] @@ -213,12 +216,46 @@ def _careplan(self): 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(' ', '')}" if id and title else str(uuid()), + 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), @@ -610,6 +647,43 @@ def _immunization_composition(self): 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( @@ -932,6 +1006,30 @@ def create_immunization_record(self): ], ).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() @@ -1076,7 +1174,7 @@ def create_record(self, record_type): elif record_type == "HealthDocumentRecord": return self.create_health_document_record() elif record_type == "DiagnosticReport": - return self.create_discharge_summary_record() + return self.create_diagnostic_report_record() elif record_type == "DischargeSummary": return self.create_discharge_summary_record() elif record_type == "OPConsultation": From 19c6819d4d177d8714bf353d46e6f9be96b6fca1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 21 Apr 2023 12:10:41 +0530 Subject: [PATCH 070/137] auto link care context while discharge --- care/abdm/api/viewsets/auth.py | 109 ++++++-------------------- care/abdm/api/viewsets/hip.py | 16 ++-- care/abdm/utils/api_call.py | 30 ++++++- care/facility/api/viewsets/patient.py | 15 ++++ 4 files changed, 76 insertions(+), 94 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 2f3c5c33ce..2fa8782e9b 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -43,11 +43,30 @@ class OnConfirmView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data print(data) - AbdmGateway().save_linking_token( - data["auth"]["patient"], - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) + + 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) @@ -80,7 +99,7 @@ def post(self, request, *args, **kwargs): for identifier in verified_identifiers: if identifier["type"] == "MOBILE": matched_by.append(identifier["value"]) - patients = patients.filter(phone_number=identifier["value"]) + patients = patients.filter(phone_number=f"+91{identifier['value']}") if identifier["type"] == "NDHM_HEALTH_NUMBER": matched_by.append(identifier["value"]) @@ -188,7 +207,7 @@ def post(self, request, *args, **kwargs): print(data) # TODO: create a seperate cache and also add a expiration time - cache.set(data["notification"]["consentDetail"]["consentId"], json.dumps(data)) + cache.set(data["notification"]["consentId"], json.dumps(data)) AbdmGateway().on_notify( { @@ -303,79 +322,3 @@ def post(self, request, *args, **kwargs): ) return Response({}, status=status.HTTP_202_ACCEPTED) - - -consent = { - "notification": { - "consentDetail": { - "consentId": "0ad38ac1-5f61-480a-b9d6-6ace3e2d2139", - "createdAt": "2023-04-11T08:01:55.554799671", - "purpose": {"text": "Care Management", "code": "CAREMGT", "refUri": None}, - "patient": {"id": "khavinshankar@sbx"}, - "consentManager": {"id": "sbx"}, - "hip": {"id": "IN3210000017", "name": "Coronasafe Care 01"}, - "hiTypes": [ - "DiagnosticReport", - "DischargeSummary", - "HealthDocumentRecord", - "ImmunizationRecord", - "OPConsultation", - "Prescription", - "WellnessRecord", - ], - "permission": { - "accessMode": "VIEW", - "dateRange": { - "from": "2023-04-11T08:01:32.774", - "to": "2023-04-11T08:01:32.774", - }, - "dataEraseAt": "2023-04-12T08:01:32.774", - "frequency": {"unit": "HOUR", "value": 1, "repeats": 0}, - }, - "careContexts": [ - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "c7134ba2-692a-40f5-a143-d306896436dd", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "56015494-bac8-486d-85b6-6f67d1708764", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "66d32279-b3b3-43e1-971e-eadc5da67e95", - }, - ], - }, - "status": "GRANTED", - "signature": "oB2JZD5CMecjW3y817dYK9pqE36yE3W+jUPtc0vfrPOMEFoYftdXAnNHxBUYZ0FKKIAJGf3erLxkzx0KE+ISFJyXX4U8OzKTBGJEjjJJ7/reDRSWnXS41D89/l8kmZHtVNsqmkje4BMKAjQylw9i8js+VaVpbgC7+NtYcSfLPWqPLnw+ppFJVKM3vrL7/w1UUrrSWB27YOX02XYj4eBtjxiLneG6fTzTT7QqrUtaYYFTU7CY1Ujwx+/q82J1sz3FGszFBe4c+1orqs2jwyLSgu73qmsySdJM1ugjjWs2Y/EBG6SnWjvfz7rvfDZ0KLfcUnWfEU5FVj8umGucAshnXA==", - "consentId": "0ad38ac1-5f61-480a-b9d6-6ace3e2d2139", - "grantAcknowledgement": False, - }, - "requestId": "12fa9195-031a-4d89-b96c-0d7e3cfd009f", - "timestamp": "2023-04-11T08:01:55.570970194", -} diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index c218d657f0..6b6d87d201 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -159,13 +159,15 @@ def add_care_context(self, request, *args, **kwargs): status=status.HTTP_404_NOT_FOUND, ) - response = AbdmGateway().add_contexts( + AbdmGateway().fetch_modes( { - "access_token": consultation.patient.abha_number.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())}", + "healthId": consultation.patient.abha_number.abha_number, + "name": consultation.patient.abha_number.name, + "gender": consultation.patient.abha_number.gender, + "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + "consultationId": consultation_id, + "purpose": "LINK", } ) - return Response(response) + + return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 47410fe729..2fc3173962 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -11,6 +11,7 @@ from django.db.models import Q from care.abdm.models import AbhaNumber +from care.facility.models.patient_consultation import PatientConsultation GATEWAY_API_URL = "https://dev.abdm.gov.in/" HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" @@ -325,9 +326,31 @@ class AbdmGateway: def __init__(self): self.api = APIGateway("abdm_gateway", None) + def add_care_context(self, access_token, request_id): + 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): data = self.temp_memory[request_id] - health_id = patient["id"] or data["healthId"] + health_id = patient and patient["id"] or data["healthId"] abha_object = AbhaNumber.objects.filter( Q(abha_number=health_id) | Q(health_id=health_id) @@ -352,7 +375,6 @@ def fetch_modes(self, data): name, gender, dateOfBirth, - patientId } """ self.temp_memory[request_id] = data @@ -364,7 +386,7 @@ def fetch_modes(self, data): ), "query": { "id": data["healthId"], - "purpose": "KYC_AND_LINK", + "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", "requester": {"type": "HIP", "id": self.hip_id}, }, } @@ -388,7 +410,7 @@ def init(self, prev_request_id): ), "query": { "id": data["healthId"], - "purpose": "KYC_AND_LINK", + "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", "authMode": "DEMOGRAPHICS", "requester": {"type": "HIP", "id": self.hip_id}, }, diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index ced0af6f4a..dca170044a 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -24,6 +24,7 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from care.abdm.utils.api_call import AbdmGateway from care.facility.api.serializers.patient import ( FacilityPatientStatsHistorySerializer, PatientDetailSerializer, @@ -454,6 +455,20 @@ def discharge_patient(self, request, *args, **kwargs): consultation=last_consultation, end_date__isnull=True ).update(end_date=current_time) + if last_consultation.patient.abha_number: + AbdmGateway().fetch_modes( + { + "healthId": last_consultation.patient.abha_number.abha_number, + "name": last_consultation.patient.abha_number.name, + "gender": last_consultation.patient.abha_number.gender, + "dateOfBirth": str( + last_consultation.patient.abha_number.date_of_birth + ), + "consultationId": last_consultation.external_id, + "purpose": "LINK", + } + ) + return Response(status=status.HTTP_200_OK) @action(detail=True, methods=["POST"]) From b9ea2a0aaf70f5ff7737148231fde8bc4ed8ebf3 Mon Sep 17 00:00:00 2001 From: Mathew Date: Mon, 24 Apr 2023 16:06:11 +0530 Subject: [PATCH 071/137] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 60dc5f6b00..08cda88a3d 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -5,12 +5,11 @@ on: push: branches: - - abdm-m1-vignesh + - abdm-m2 paths-ignore: - "docs/**" -env: - IMAGE_NAME: care-${{ github.ref_name}} + jobs: build-image: name: Build & Push Staging to container registries @@ -25,8 +24,8 @@ jobs: images: | ghcr.io/${{ github.repository }} tags: | - type=raw,value=latest-${{ github.run_number }} - type=raw,value=latest + type=raw,value=${{ github.ref_name}}-${{ github.run_number }} + type=raw,value=${{ github.ref_name}} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} flavor: | From b6b8ac233d9e89c1a4caa897d14e81d3429c6e57 Mon Sep 17 00:00:00 2001 From: Mathew Date: Mon, 24 Apr 2023 16:12:11 +0530 Subject: [PATCH 072/137] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 08cda88a3d..0ebe252197 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -26,10 +26,7 @@ jobs: tags: | type=raw,value=${{ github.ref_name}}-${{ github.run_number }} type=raw,value=${{ github.ref_name}} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=true + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 From 2d20aa0d90381dec3a120f9074c2221835002167 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 18:50:54 +0530 Subject: [PATCH 073/137] disable existing abha number check --- care/abdm/api/viewsets/healthid.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 553de6278e..142c324a52 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -296,13 +296,13 @@ def link_via_qr(self, request): 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, - ) + # if patient: + # return Response( + # { + # "message": "A patient is already associated with the provided Abha Number" + # }, + # status=status.HTTP_400_BAD_REQUEST, + # ) if ( "facilityId" not in data From 9e32644500c2470f7b2326ee704293beaa55b27a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 19:14:18 +0530 Subject: [PATCH 074/137] pick latest patient if more than 1 in discover api --- care/abdm/api/viewsets/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 2fa8782e9b..458c245a27 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -113,11 +113,11 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - patients.filter( + patients = patients.filter( abha_number__name=data["patient"]["name"], abha_number__gender=data["patient"]["gender"], # TODO: check date also - ) + ).last() if len(patients) != 1: return Response( From 6bf96a0f727adca11bd27a478944e6f34d114e8e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 19:25:08 +0530 Subject: [PATCH 075/137] removed multiple patient condition check in discover --- care/abdm/api/viewsets/auth.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 458c245a27..afd66989a0 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -113,31 +113,31 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - patients = patients.filter( + patient = patients.filter( abha_number__name=data["patient"]["name"], abha_number__gender=data["patient"]["gender"], # TODO: check date also ).last() - if len(patients) != 1: - return Response( - "No matching records found, need more data", - status=status.HTTP_404_NOT_FOUND, - ) + # if len(patients) != 1: + # 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(patients[0].external_id), - "patient_name": patients[0].name, + "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=patients[0]), + PatientConsultation.objects.filter(patient=patient), ) ), "matched_by": matched_by, From c5ff93bc8a5cb27dcb615635deef946cdc714822 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 19:39:33 +0530 Subject: [PATCH 076/137] select last patient directly in discover --- care/abdm/api/viewsets/auth.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index afd66989a0..6688421c22 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -113,11 +113,13 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - patient = patients.filter( - abha_number__name=data["patient"]["name"], - abha_number__gender=data["patient"]["gender"], - # TODO: check date also - ).last() + # patient = patients.filter( + # abha_number__name=data["patient"]["name"], + # abha_number__gender=data["patient"]["gender"], + # # TODO: check date also + # ).last() + + patient = patients.last() # if len(patients) != 1: # return Response( From 307077d4707a3149c952ba7770a494cf6e4a9135 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 20:00:47 +0530 Subject: [PATCH 077/137] added prints --- care/abdm/api/viewsets/auth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 6688421c22..9cf9a263c0 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -88,6 +88,7 @@ def post(self, request, *args, **kwargs): data = request.data patients = PatientRegistration.objects.all() + print(data, patients) verified_identifiers = data["patient"]["verifiedIdentifiers"] matched_by = [] if len(verified_identifiers) == 0: @@ -118,8 +119,10 @@ def post(self, request, *args, **kwargs): # abha_number__gender=data["patient"]["gender"], # # TODO: check date also # ).last() + print(patients) patient = patients.last() + print(patient) # if len(patients) != 1: # return Response( From 3136a9e8bcd1dfb436f62ba70759c9f78cad577a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 20:12:10 +0530 Subject: [PATCH 078/137] improved checks for discover api --- care/abdm/api/viewsets/auth.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 9cf9a263c0..48ec6bfd20 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta from django.core.cache import cache +from django.db.models import Q from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny @@ -88,7 +89,6 @@ def post(self, request, *args, **kwargs): data = request.data patients = PatientRegistration.objects.all() - print(data, patients) verified_identifiers = data["patient"]["verifiedIdentifiers"] matched_by = [] if len(verified_identifiers) == 0: @@ -100,7 +100,10 @@ def post(self, request, *args, **kwargs): for identifier in verified_identifiers: if identifier["type"] == "MOBILE": matched_by.append(identifier["value"]) - patients = patients.filter(phone_number=f"+91{identifier['value']}") + patients = patients.filter( + Q(phone_number=f"+91{identifier['value']}") + | Q(health_id=identifier["value"]) + ) if identifier["type"] == "NDHM_HEALTH_NUMBER": matched_by.append(identifier["value"]) @@ -114,21 +117,17 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - # patient = patients.filter( - # abha_number__name=data["patient"]["name"], - # abha_number__gender=data["patient"]["gender"], - # # TODO: check date also - # ).last() - print(patients) - - patient = patients.last() - print(patient) - - # if len(patients) != 1: - # return Response( - # "No matching records found, need more data", - # status=status.HTTP_404_NOT_FOUND, - # ) + patient = patients.filter( + abha_number__name=data["patient"]["name"], + abha_number__gender=data["patient"]["gender"], + # TODO: check date also + ).last() + + if not patient: + return Response( + "No matching records found, need more data", + status=status.HTTP_404_NOT_FOUND, + ) AbdmGateway().on_discover( { From 2d52f76de6fe7ca352926188edad967e0c5c3e35 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 20:18:12 +0530 Subject: [PATCH 079/137] fixed a bug in query in discover api --- care/abdm/api/viewsets/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 48ec6bfd20..74375ea759 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -102,7 +102,7 @@ def post(self, request, *args, **kwargs): matched_by.append(identifier["value"]) patients = patients.filter( Q(phone_number=f"+91{identifier['value']}") - | Q(health_id=identifier["value"]) + | Q(phone_number=identifier["value"]) ) if identifier["type"] == "NDHM_HEALTH_NUMBER": From cec1ce23db7d2a865d607a19254de3b24ffe3214 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 25 Apr 2023 10:58:15 +0530 Subject: [PATCH 080/137] turned on debug and linked a s3 bucket --- config/settings/base.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index da7fd307f8..fe860d98c4 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -29,7 +29,7 @@ # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = env.bool("DJANGO_DEBUG", False) +DEBUG = env.bool("DJANGO_DEBUG", True) # Local time zone. Choices are # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. @@ -411,15 +411,20 @@ def GETKEY(group, request): ####################### # File Upload Parameters - -FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") -# FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default="care-patient-staging") -FILE_UPLOAD_KEY = env("FILE_UPLOAD_KEY", default="") -FILE_UPLOAD_SECRET = env("FILE_UPLOAD_SECRET", default="") -FILE_UPLOAD_BUCKET_ENDPOINT = env( - "FILE_UPLOAD_BUCKET_ENDPOINT", - default=f"https://{FILE_UPLOAD_BUCKET}.s3.amazonaws.com", -) +FILE_UPLOAD_BUCKET_ENDPOINT = "https://care-s3-dev.s3.amazonaws.com" +FILE_UPLOAD_KEY = "AKIAULRENCOFV7LFUO74" +FILE_UPLOAD_SECRET = "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" +FILE_UPLOAD_BUCKET = "care-s3-dev" +CLOUD_REGION = "ap-south-1" +FACILITY_S3_BUCKET_ENDPOINT = "https://care-s3-dev.s3.amazonaws.com" +# FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") +# # FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default="care-patient-staging") +# FILE_UPLOAD_KEY = env("FILE_UPLOAD_KEY", default="") +# FILE_UPLOAD_SECRET = env("FILE_UPLOAD_SECRET", default="") +# FILE_UPLOAD_BUCKET_ENDPOINT = env( +# "FILE_UPLOAD_BUCKET_ENDPOINT", +# default=f"https://{FILE_UPLOAD_BUCKET}.s3.amazonaws.com", +# ) FACILITY_S3_BUCKET = env("FACILITY_S3_BUCKET", default="") FACILITY_S3_REGION = env("FACILITY_S3_REGION_CODE", default="ap-south-1") From 85d0ed7ef368f73bbd3aea3da7f57185ab577eb9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 26 Apr 2023 09:18:57 +0530 Subject: [PATCH 081/137] handled None case in immunization record and empty string case in practitioner name --- care/abdm/utils/fhir.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index c71d5cd811..b28ab3eb70 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -46,6 +46,7 @@ def __init__(self, consultation): 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 = [] @@ -86,7 +87,10 @@ def _practioner(self): return self._practitioner_profile id = str(uuid()) - name = self.consultation.verified_by + 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)], @@ -384,10 +388,13 @@ def _encounter(self, include_diagnosis=False): return self._encounter_profile def _immunization(self): + if self._immunization_profile: + return self._immunization_profile + if not self.consultation.patient.is_vaccinated: return - return Immunization( + self._immunization_profile = Immunization( id=str(uuid()), status="completed", identifier=[ @@ -639,7 +646,18 @@ def _immunization_composition(self): ), ], ), - entry=[self._reference(self._immunization())], + 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()), From 11cfa4d0293efffe6f840a4e9f1dec9cf9a5f203 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 26 Apr 2023 09:25:03 +0530 Subject: [PATCH 082/137] moved add_are_context inti healthid viewset --- care/abdm/api/viewsets/healthid.py | 27 ++++++++++++++++++++++++++- care/abdm/api/viewsets/hip.py | 28 +--------------------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 142c324a52..50c1bf864e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -26,7 +26,7 @@ from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.api.serializers.patient import PatientDetailSerializer from care.facility.models.facility import Facility -from care.facility.models.patient import PatientRegistration +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 @@ -408,6 +408,31 @@ def get_new_linking_token(self, request): 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"] + + 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.abha_number, + "name": consultation.patient.abha_number.name, + "gender": consultation.patient.abha_number.gender, + "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + "consultationId": consultation_id, + "purpose": "LINK", + } + ) + + return Response(status=status.HTTP_202_ACCEPTED) + # auth/init @swagger_auto_schema( # /v1/auth/init diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 6b6d87d201..09945866f9 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -11,7 +11,7 @@ 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 PatientConsultation, PatientRegistration +from care.facility.models.patient import PatientRegistration class HipViewSet(GenericViewSet): @@ -145,29 +145,3 @@ def share(self, request, *args, **kwargs): }, status=status.HTTP_401_UNAUTHORIZED, ) - - # TODO: move it somewhere appropriate - @action(detail=False, methods=["POST"]) - def add_care_context(self, request, *args, **kwargs): - consultation_id = request.data["consultation"] - - 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.abha_number, - "name": consultation.patient.abha_number.name, - "gender": consultation.patient.abha_number.gender, - "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), - "consultationId": consultation_id, - "purpose": "LINK", - } - ) - - return Response(status=status.HTTP_202_ACCEPTED) From 67c4be5ffe1e93b2840df92819d0cdf06e10d8a9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 27 Apr 2023 08:46:01 +0530 Subject: [PATCH 083/137] added heartbeat api --- care/abdm/api/viewsets/monitoring.py | 23 +++++++++++++++++++++++ config/urls.py | 6 ++++++ 2 files changed, 29 insertions(+) create mode 100644 care/abdm/api/viewsets/monitoring.py 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/config/urls.py b/config/urls.py index 280d6509a1..8050b1aaa8 100644 --- a/config/urls.py +++ b/config/urls.py @@ -20,6 +20,7 @@ OnInitView, RequestDataView, ) +from care.abdm.api.viewsets.monitoring import HeartbeatView from care.facility.api.viewsets.open_id import OpenIdConfigView from care.hcx.api.viewsets.listener import ( ClaimOnSubmitView, @@ -129,6 +130,11 @@ RequestDataView.as_view(), name="abdm_request_data_view", ), + path( + "v0.5/heartbeat", + HeartbeatView.as_view(), + name="abdm_monitoring_heartbeat_view", + ), # Hcx Listeners path( "coverageeligibility/on_check", From 949f079897caa1c9e1213ca79826d25a9d56032a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 27 Apr 2023 09:23:44 +0530 Subject: [PATCH 084/137] added abha opt out flow --- care/abdm/api/viewsets/status.py | 28 ++++++++++++++++++++++++++++ care/abdm/utils/api_call.py | 18 ++++++++++++++++++ config/urls.py | 6 ++++++ 3 files changed, 52 insertions(+) create mode 100644 care/abdm/api/viewsets/status.py diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py new file mode 100644 index 0000000000..a1f56ab96e --- /dev/null +++ b/care/abdm/api/viewsets/status.py @@ -0,0 +1,28 @@ +from rest_framework import status +from rest_framework.generics import GenericAPIView +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + +from care.abdm.models import AbhaNumber +from care.abdm.utils.api_call import AbdmGateway +from care.facility.models.patient import PatientRegistration + + +class NotifyView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print("patient_status_notify", 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) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 2fc3173962..8923050bee 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -673,6 +673,24 @@ def data_notify(self, data): 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": str( + 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 + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/config/urls.py b/config/urls.py index 8050b1aaa8..23c4712911 100644 --- a/config/urls.py +++ b/config/urls.py @@ -21,6 +21,7 @@ RequestDataView, ) from care.abdm.api.viewsets.monitoring import HeartbeatView +from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView from care.facility.api.viewsets.open_id import OpenIdConfigView from care.hcx.api.viewsets.listener import ( ClaimOnSubmitView, @@ -130,6 +131,11 @@ 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/heartbeat", HeartbeatView.as_view(), From 48cb737fa842c12efffd5af106b69b9ca841108a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 27 Apr 2023 10:13:39 +0530 Subject: [PATCH 085/137] added sms apis --- care/abdm/api/viewsets/status.py | 11 +++++++++++ care/abdm/utils/api_call.py | 20 ++++++++++++++++++++ config/urls.py | 6 ++++++ 3 files changed, 37 insertions(+) diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py index a1f56ab96e..a52044c5e4 100644 --- a/care/abdm/api/viewsets/status.py +++ b/care/abdm/api/viewsets/status.py @@ -26,3 +26,14 @@ def post(self, request, *args, **kwargs): AbdmGateway().patient_status_on_notify({"request_id": data["requestId"]}) return Response(status=status.HTTP_202_ACCEPTED) + + +class SMSOnNotifyView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print("patient_sms_on_notify", data) + + return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 8923050bee..b02746e4ec 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -321,6 +321,7 @@ def verify_document_mobile_otp(self, data): class AbdmGateway: # TODO: replace this with in-memory db (redis) temp_memory = {} + hip_name = "Coronasafe Care 01" hip_id = "IN3210000017" def __init__(self): @@ -691,6 +692,25 @@ def patient_status_on_notify(self, data): 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": str( + 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" diff --git a/config/urls.py b/config/urls.py index 23c4712911..0e325edc4a 100644 --- a/config/urls.py +++ b/config/urls.py @@ -22,6 +22,7 @@ ) 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 from care.facility.api.viewsets.open_id import OpenIdConfigView from care.hcx.api.viewsets.listener import ( ClaimOnSubmitView, @@ -136,6 +137,11 @@ 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(), From b37d44e2c0dfa48381d6f72c80df44ca6e484d5f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 24 May 2023 12:19:14 +0530 Subject: [PATCH 086/137] added ratelimiting to m2 apis --- care/abdm/api/viewsets/healthid.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 50c1bf864e..b28d4ce869 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -287,6 +287,12 @@ def search_by_health_id(self, request): def link_via_qr(self, request): data = request.data + if ratelimit(request, "link_via_qr", [data["hdin"]], 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) @@ -393,6 +399,14 @@ def link_via_qr(self, request): def get_new_linking_token(self, request): data = request.data + if ratelimit( + request, "get_new_linking_token", [data["patient"]], increment=False + ): + 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 @@ -412,6 +426,12 @@ def get_new_linking_token(self, request): def add_care_context(self, request, *args, **kwargs): consultation_id = request.data["consultation"] + if ratelimit(request, "add_care_context", [consultation_id], increment=False): + 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: From a8a416282979aae08e59f2d3d90875243d95fe62 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 3 Jun 2023 12:21:45 +0530 Subject: [PATCH 087/137] added patient_sms_notify --- care/abdm/api/viewsets/healthid.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index b28d4ce869..14b854212c 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -453,6 +453,28 @@ def add_care_context(self, request, *args, **kwargs): 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], increment=False): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + patient = PatientRegistration.objects.get(external_id=patient_id) + + if not patient: + return Response( + {"consultation": "No matching records found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + AbdmGateway().patient_sms_notify({"phone": patient.phone_number}) + + return Response(status=status.HTTP_202_ACCEPTED) + # auth/init @swagger_auto_schema( # /v1/auth/init From 8ccc1b7733dd57c8ecbdfbf93dee2992990fa28f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 3 Jun 2023 12:44:06 +0530 Subject: [PATCH 088/137] return abdm response in patient_sms_notify --- care/abdm/api/viewsets/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 14b854212c..305827da33 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -471,9 +471,9 @@ def patient_sms_notify(self, request, *args, **kwargs): status=status.HTTP_404_NOT_FOUND, ) - AbdmGateway().patient_sms_notify({"phone": patient.phone_number}) + response = AbdmGateway().patient_sms_notify({"phone": patient.phone_number}) - return Response(status=status.HTTP_202_ACCEPTED) + return Response(response, status=status.HTTP_202_ACCEPTED) # auth/init @swagger_auto_schema( From 3cd09d818d242ceb0738a4558ddef981a90a4bb0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 4 Jun 2023 08:43:26 +0530 Subject: [PATCH 089/137] added key chech in auth/init and auth/confirm --- care/abdm/utils/api_call.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index b02746e4ec..ba1ff86fc4 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -396,6 +396,9 @@ def fetch_modes(self, data): # "/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} @@ -421,6 +424,9 @@ def init(self, prev_request_id): # "/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} From 66b231638bcfe7df1675f4d5e287ae557753dde7 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 4 Jun 2023 08:58:17 +0530 Subject: [PATCH 090/137] added key chech in save_linking_token and add_care_context --- care/abdm/utils/api_call.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index ba1ff86fc4..201bc47854 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -328,6 +328,9 @@ 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: @@ -350,6 +353,9 @@ def add_care_context(self, access_token, request_id): 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"] From 72240a2803d162c4073a4a70791fe6ba8a8ad4f1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 4 Jun 2023 10:35:27 +0530 Subject: [PATCH 091/137] minor fail safes added --- care/abdm/api/viewsets/auth.py | 8 +++++--- care/abdm/api/viewsets/healthid.py | 2 +- care/abdm/api/viewsets/hip.py | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 74375ea759..0fdcf145ea 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -4,7 +4,7 @@ from django.core.cache import cache from django.db.models import Q from rest_framework import status -from rest_framework.generics import GenericAPIView +from rest_framework.generics import GenericAPIView, get_object_or_404 from rest_framework.permissions import AllowAny from rest_framework.response import Response @@ -179,8 +179,10 @@ def post(self, request, *args, **kwargs): # TODO: verify otp - patient = PatientRegistration.objects.get( - external_id=data["confirmation"]["linkRefNumber"] + patient = get_object_or_404( + PatientRegistration.objects.filter( + external_id=data["confirmation"]["linkRefNumber"] + ).first() ) AbdmGateway().on_link_confirm( { diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 305827da33..f583baca8d 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -463,7 +463,7 @@ def patient_sms_notify(self, request, *args, **kwargs): code=status.HTTP_429_TOO_MANY_REQUESTS, ) - patient = PatientRegistration.objects.get(external_id=patient_id) + patient = PatientRegistration.objects.filter(external_id=patient_id).first() if not patient: return Response( diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 09945866f9..f22415f72b 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -25,6 +25,7 @@ def get_linking_token(self, data): @action(detail=False, methods=["POST"]) def share(self, request, *args, **kwargs): data = request.data + print(data) patient_data = data["profile"]["patient"] counter_id = ( From b562bb52788599678593d38e012716ff74ccf1e2 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 5 Jun 2023 14:45:32 +0530 Subject: [PATCH 092/137] enabled ratelimiting --- config/settings/local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings/local.py b/config/settings/local.py index 288db0a97a..7c98c8ce5f 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -100,7 +100,7 @@ AUDIT_LOG_ENABLED = True -DISABLE_RATELIMIT = True +DISABLE_RATELIMIT = False FILE_UPLOAD_BUCKET_ENDPOINT = "http://localstack:4566" From 7c574719b4f42c30c90da24a2e088cf198017229 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 5 Jun 2023 15:07:47 +0530 Subject: [PATCH 093/137] debug: ratelimit --- config/ratelimit.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/ratelimit.py b/config/ratelimit.py index 435076c726..cd751b7a47 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -25,9 +25,15 @@ def validatecaptcha(request): def ratelimit( request, group="", keys=[None], rate=settings.DJANGO_RATE_LIMIT, increment=True ): + print("---------------------------------------------------------") + print(settings.DISABLE_RATELIMIT) + print("---------------------------------------------------------") if settings.DISABLE_RATELIMIT: return False + print("---------------------------------------------------------") + print(group, keys, rate) + print("---------------------------------------------------------") checkcaptcha = False for key in keys: if key == "ip": From c71012b99811c7277da73ebf27d41bebe665055d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 5 Jun 2023 15:16:33 +0530 Subject: [PATCH 094/137] removed increament in ratelimiting --- care/abdm/api/viewsets/healthid.py | 8 +++----- config/ratelimit.py | 6 ------ config/settings/local.py | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index f583baca8d..9a75bcda5e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -399,9 +399,7 @@ def link_via_qr(self, request): def get_new_linking_token(self, request): data = request.data - if ratelimit( - request, "get_new_linking_token", [data["patient"]], increment=False - ): + 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, @@ -426,7 +424,7 @@ def get_new_linking_token(self, request): def add_care_context(self, request, *args, **kwargs): consultation_id = request.data["consultation"] - if ratelimit(request, "add_care_context", [consultation_id], increment=False): + 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, @@ -457,7 +455,7 @@ def add_care_context(self, request, *args, **kwargs): def patient_sms_notify(self, request, *args, **kwargs): patient_id = request.data["patient"] - if ratelimit(request, "patient_sms_notify", [patient_id], increment=False): + 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, diff --git a/config/ratelimit.py b/config/ratelimit.py index cd751b7a47..435076c726 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -25,15 +25,9 @@ def validatecaptcha(request): def ratelimit( request, group="", keys=[None], rate=settings.DJANGO_RATE_LIMIT, increment=True ): - print("---------------------------------------------------------") - print(settings.DISABLE_RATELIMIT) - print("---------------------------------------------------------") if settings.DISABLE_RATELIMIT: return False - print("---------------------------------------------------------") - print(group, keys, rate) - print("---------------------------------------------------------") checkcaptcha = False for key in keys: if key == "ip": diff --git a/config/settings/local.py b/config/settings/local.py index 7c98c8ce5f..288db0a97a 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -100,7 +100,7 @@ AUDIT_LOG_ENABLED = True -DISABLE_RATELIMIT = False +DISABLE_RATELIMIT = True FILE_UPLOAD_BUCKET_ENDPOINT = "http://localstack:4566" From d0c2e2d2f3cb5f63b0b7887fd1f73fa243037d6e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 7 Jun 2023 11:17:36 +0530 Subject: [PATCH 095/137] added ABDMAuthentication for authentication the callbacks --- care/abdm/api/viewsets/auth.py | 19 ++++++------ care/abdm/api/viewsets/hip.py | 3 +- care/abdm/api/viewsets/status.py | 5 +-- config/authentication.py | 52 ++++++++++++++++++++++++++++++++ config/settings/base.py | 2 ++ 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 0fdcf145ea..b978c835d1 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -13,11 +13,12 @@ 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 = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -28,7 +29,7 @@ def post(self, request, *args, **kwargs): class OnInitView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -39,7 +40,7 @@ def post(self, request, *args, **kwargs): class OnConfirmView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -73,7 +74,7 @@ def post(self, request, *args, **kwargs): class OnAddContextsView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -83,7 +84,7 @@ def post(self, request, *args, **kwargs): class DiscoverView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -152,7 +153,7 @@ def post(self, request, *args, **kwargs): class LinkInitView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -172,7 +173,7 @@ def post(self, request, *args, **kwargs): class LinkConfirmView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -206,7 +207,7 @@ def post(self, request, *args, **kwargs): class NotifyView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -226,7 +227,7 @@ def post(self, request, *args, **kwargs): class RequestDataView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index f22415f72b..cd625fb246 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -12,11 +12,12 @@ 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 = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def get_linking_token(self, data): AbdmGateway().fetch_modes(data) diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py index a52044c5e4..3da724c722 100644 --- a/care/abdm/api/viewsets/status.py +++ b/care/abdm/api/viewsets/status.py @@ -6,11 +6,12 @@ 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 = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -30,7 +31,7 @@ def post(self, request, *args, **kwargs): class SMSOnNotifyView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data diff --git a/config/authentication.py b/config/authentication.py index 8a68ca592e..a03e2daa2b 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -2,6 +2,7 @@ import jwt import requests +from django.conf import settings from django.contrib.auth import authenticate from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -178,3 +179,54 @@ def get_user(self, validated_token, facility): ) asset_user.save() 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 diff --git a/config/settings/base.py b/config/settings/base.py index fe860d98c4..a6c49d6ea5 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -519,5 +519,7 @@ def GETKEY(group, request): 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") +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") From e561bc26a4ab7381a22c5d5a32eb65d4abafa353 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 7 Jun 2023 12:23:39 +0530 Subject: [PATCH 096/137] set jwt as default auth in production --- config/settings/production.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/settings/production.py b/config/settings/production.py index d8580b51cd..d48e5fcdd4 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -1,7 +1,8 @@ from .deployment import * # noqa -# Your stuff... -# ------------------------------------------------------------------------------ +REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( # noqa F405 + "config.authentication.CustomJWTAuthentication", +) IS_PRODUCTION = True USE_SMS = True From 3e33b1d5c78526c08d75e33b5d8312f99021c204 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 8 Jun 2023 11:25:26 +0530 Subject: [PATCH 097/137] fixed issue in link/confirm callback --- care/abdm/api/viewsets/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index b978c835d1..7f7be41f94 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -183,7 +183,7 @@ def post(self, request, *args, **kwargs): patient = get_object_or_404( PatientRegistration.objects.filter( external_id=data["confirmation"]["linkRefNumber"] - ).first() + ) ) AbdmGateway().on_link_confirm( { From f9303be540ef305c60cd5b74b48564fbeb499aa1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 8 Jun 2023 11:30:45 +0530 Subject: [PATCH 098/137] temp: send 202 for hip/request and profile/share --- care/abdm/api/viewsets/auth.py | 5 +++-- care/abdm/api/viewsets/hip.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7f7be41f94..892b608e01 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -236,8 +236,8 @@ def post(self, request, *args, **kwargs): # TODO: uncomment later 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) + # 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( @@ -255,6 +255,7 @@ def post(self, request, *args, **kwargs): ) 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 ) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index cd625fb246..4958e23d94 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -140,6 +140,14 @@ def share(self, request, *args, **kwargs): status=status.HTTP_202_ACCEPTED, ) + return Response( + { + "status": "ACCEPTED", + "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], + }, + status=status.HTTP_202_ACCEPTED, + ) + return Response( { "status": "FAILURE", From be6968f48cec8e89f57a44bff644c6dd6fce68c2 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 8 Jun 2023 18:32:24 +0530 Subject: [PATCH 099/137] set debug to false --- config/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings/base.py b/config/settings/base.py index a6c49d6ea5..db342ce483 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -29,7 +29,7 @@ # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = env.bool("DJANGO_DEBUG", True) +DEBUG = env.bool("DJANGO_DEBUG", False) # Local time zone. Choices are # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. From e57e8178df8b2f2d85e1db37dc314010bb68193e Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 9 Jun 2023 13:47:35 +0530 Subject: [PATCH 100/137] add external_id field to users --- .../users/migrations/0047_user_external_id.py | 19 ++++++++++++++++ .../migrations/0048_auto_20230609_1411.py | 22 +++++++++++++++++++ .../migrations/0049_auto_20230609_1413.py | 19 ++++++++++++++++ care/users/models.py | 3 +++ 4 files changed, 63 insertions(+) create mode 100644 care/users/migrations/0047_user_external_id.py create mode 100644 care/users/migrations/0048_auto_20230609_1411.py create mode 100644 care/users/migrations/0049_auto_20230609_1413.py diff --git a/care/users/migrations/0047_user_external_id.py b/care/users/migrations/0047_user_external_id.py new file mode 100644 index 0000000000..86b0ecc94f --- /dev/null +++ b/care/users/migrations/0047_user_external_id.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.11 on 2023-06-09 08:41 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0046_auto_20230204_1733"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="external_id", + field=models.UUIDField(default=uuid.uuid4, null=True), + ), + ] diff --git a/care/users/migrations/0048_auto_20230609_1411.py b/care/users/migrations/0048_auto_20230609_1411.py new file mode 100644 index 0000000000..7b2f7fcee3 --- /dev/null +++ b/care/users/migrations/0048_auto_20230609_1411.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.11 on 2023-06-09 08:41 + +import uuid + +from django.db import migrations + + +def gen_uuid(apps, schema_editor): + User = apps.get_model("users", "User") + for user in User.objects.get_entire_queryset(): + user.external_id = uuid.uuid4() + user.save(update_fields=["external_id"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0047_user_external_id"), + ] + + operations = [ + migrations.RunPython(gen_uuid, migrations.RunPython.noop), + ] diff --git a/care/users/migrations/0049_auto_20230609_1413.py b/care/users/migrations/0049_auto_20230609_1413.py new file mode 100644 index 0000000000..a1789c58bf --- /dev/null +++ b/care/users/migrations/0049_auto_20230609_1413.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.11 on 2023-06-09 08:43 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0048_auto_20230609_1411"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="external_id", + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ] diff --git a/care/users/models.py b/care/users/models.py index a9401ede93..166460aa0f 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -1,3 +1,5 @@ +import uuid + from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.validators import UnicodeUsernameValidator from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator @@ -172,6 +174,7 @@ class Meta: class User(AbstractUser): + external_id = models.UUIDField(default=uuid.uuid4, unique=True, db_index=True) username_validator = UsernameValidator() username = models.CharField( _("username"), From 69b3996767e19d8a6a0d72f23625f3178a443a2b Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 9 Jun 2023 14:21:49 +0530 Subject: [PATCH 101/137] use uuid for jwt --- config/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings/base.py b/config/settings/base.py index db342ce483..d6b91fc5d9 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -318,6 +318,7 @@ minutes=env("JWT_REFRESH_TOKEN_LIFETIME", default=30) ), "ROTATE_REFRESH_TOKENS": True, + "USER_ID_FIELD": "external_id", } From 35170453411e0df0effebd579782861410598f4d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 9 Jun 2023 14:59:43 +0530 Subject: [PATCH 102/137] use uuid for user_id in jwt in local conf --- config/settings/local.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings/local.py b/config/settings/local.py index 288db0a97a..3e917901d8 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -94,6 +94,7 @@ minutes=env("JWT_REFRESH_TOKEN_LIFETIME", default=3000000000) ), "ROTATE_REFRESH_TOKENS": True, + "USER_ID_FIELD": "external_id", } RUNSERVER_PLUS_PRINT_SQL_TRUNCATE = 100000 From 08e38501fced37bcf6560ccdedc219a1f94a4c1c Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 9 Jun 2023 20:16:40 +0530 Subject: [PATCH 103/137] fix merge issues --- .../migrations/0361_merge_20230609_2014.py | 12 +++++++ config/settings/base.py | 31 ++++++++++--------- 2 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 care/facility/migrations/0361_merge_20230609_2014.py diff --git a/care/facility/migrations/0361_merge_20230609_2014.py b/care/facility/migrations/0361_merge_20230609_2014.py new file mode 100644 index 0000000000..66a67446c3 --- /dev/null +++ b/care/facility/migrations/0361_merge_20230609_2014.py @@ -0,0 +1,12 @@ +# Generated by Django 2.2.11 on 2023-06-09 14:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0360_auto_20230608_1750"), + ("facility", "0344_merge_20230413_1120"), + ] + + operations = [] diff --git a/config/settings/base.py b/config/settings/base.py index 1c0d4173a4..18b50651bf 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -412,20 +412,14 @@ def GETKEY(group, request): ####################### # File Upload Parameters -FILE_UPLOAD_BUCKET_ENDPOINT = "https://care-s3-dev.s3.amazonaws.com" -FILE_UPLOAD_KEY = "AKIAULRENCOFV7LFUO74" -FILE_UPLOAD_SECRET = "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" -FILE_UPLOAD_BUCKET = "care-s3-dev" -CLOUD_REGION = "ap-south-1" -FACILITY_S3_BUCKET_ENDPOINT = "https://care-s3-dev.s3.amazonaws.com" -# FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") -# # FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default="care-patient-staging") -# FILE_UPLOAD_KEY = env("FILE_UPLOAD_KEY", default="") -# FILE_UPLOAD_SECRET = env("FILE_UPLOAD_SECRET", default="") -# FILE_UPLOAD_BUCKET_ENDPOINT = env( -# "FILE_UPLOAD_BUCKET_ENDPOINT", -# default=f"https://{FILE_UPLOAD_BUCKET}.s3.amazonaws.com", -# ) +FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") +FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default="care-patient-staging") +FILE_UPLOAD_KEY = env("FILE_UPLOAD_KEY", default="") +FILE_UPLOAD_SECRET = env("FILE_UPLOAD_SECRET", default="") +FILE_UPLOAD_BUCKET_ENDPOINT = env( + "FILE_UPLOAD_BUCKET_ENDPOINT", + default=f"https://{FILE_UPLOAD_BUCKET}.s3.amazonaws.com", +) FACILITY_S3_BUCKET = env("FACILITY_S3_BUCKET", default="") FACILITY_S3_REGION = env("FACILITY_S3_REGION_CODE", default="ap-south-1") @@ -517,3 +511,12 @@ def GETKEY(group, request): JWKS = JsonWebKey.import_key_set( json.loads(base64.b64decode(env("JWKS_BASE64", default=generate_encoded_jwks()))) ) + +PEACETIME_MODE = env.bool("PEACETIME_MODE", default=True) + +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") +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") From bfbc965881953ee6b25b871c3b4add1757b8bd7c Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 9 Jun 2023 20:20:33 +0530 Subject: [PATCH 104/137] fix lint issues --- care/abdm/migrations/0001_initial.py | 56 ++++++++++++------- .../migrations/0002_auto_20221220_2312.py | 19 +++---- .../migrations/0003_auto_20221220_2321.py | 11 ++-- .../migrations/0004_auto_20221220_2325.py | 35 ++++++------ .../migrations/0005_auto_20221220_2327.py | 43 +++++++------- .../migrations/0006_auto_20230208_0915.py | 1 - .../migrations/0329_auto_20221219_1936.py | 28 +++++++--- .../migrations/0330_auto_20221220_2312.py | 31 ++++++---- .../migrations/0331_auto_20230130_1652.py | 7 +-- .../migrations/0344_merge_20230413_1120.py | 8 +-- 10 files changed, 133 insertions(+), 106 deletions(-) diff --git a/care/abdm/migrations/0001_initial.py b/care/abdm/migrations/0001_initial.py index cb8481db18..bcd8410442 100644 --- a/care/abdm/migrations/0001_initial.py +++ b/care/abdm/migrations/0001_initial.py @@ -8,32 +8,48 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='AbhaNumber', + name="AbhaNumber", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), - ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), - ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), - ('deleted', models.BooleanField(db_index=True, default=False)), - ('abha_number', models.CharField(max_length=50)), - ('email', models.CharField(max_length=50)), - ('first_name', models.CharField(max_length=50)), - ('health_id', models.CharField(max_length=50)), - ('last_name', models.CharField(max_length=50)), - ('middle_name', models.CharField(max_length=50)), - ('password', models.CharField(max_length=50)), - ('profile_photo', models.CharField(max_length=50)), - ('txn_id', models.CharField(max_length=50)), - ('access_token', models.CharField(max_length=50)), - ('refresh_token', models.CharField(max_length=50)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("abha_number", models.CharField(max_length=50)), + ("email", models.CharField(max_length=50)), + ("first_name", models.CharField(max_length=50)), + ("health_id", models.CharField(max_length=50)), + ("last_name", models.CharField(max_length=50)), + ("middle_name", models.CharField(max_length=50)), + ("password", models.CharField(max_length=50)), + ("profile_photo", models.CharField(max_length=50)), + ("txn_id", models.CharField(max_length=50)), + ("access_token", models.CharField(max_length=50)), + ("refresh_token", models.CharField(max_length=50)), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/care/abdm/migrations/0002_auto_20221220_2312.py b/care/abdm/migrations/0002_auto_20221220_2312.py index fff590d3d5..73253c5c75 100644 --- a/care/abdm/migrations/0002_auto_20221220_2312.py +++ b/care/abdm/migrations/0002_auto_20221220_2312.py @@ -4,30 +4,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('abdm', '0001_initial'), + ("abdm", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='abhanumber', - name='email', + model_name="abhanumber", + name="email", field=models.EmailField(max_length=254), ), migrations.AlterField( - model_name='abhanumber', - name='first_name', + model_name="abhanumber", + name="first_name", field=models.CharField(max_length=512), ), migrations.AlterField( - model_name='abhanumber', - name='last_name', + model_name="abhanumber", + name="last_name", field=models.CharField(max_length=512), ), migrations.AlterField( - model_name='abhanumber', - name='middle_name', + model_name="abhanumber", + name="middle_name", field=models.CharField(max_length=512), ), ] diff --git a/care/abdm/migrations/0003_auto_20221220_2321.py b/care/abdm/migrations/0003_auto_20221220_2321.py index 7e2fd3121b..aaa8357d2c 100644 --- a/care/abdm/migrations/0003_auto_20221220_2321.py +++ b/care/abdm/migrations/0003_auto_20221220_2321.py @@ -4,19 +4,18 @@ class Migration(migrations.Migration): - dependencies = [ - ('abdm', '0002_auto_20221220_2312'), + ("abdm", "0002_auto_20221220_2312"), ] operations = [ migrations.RemoveField( - model_name='abhanumber', - name='password', + model_name="abhanumber", + name="password", ), migrations.AlterField( - model_name='abhanumber', - name='profile_photo', + model_name="abhanumber", + name="profile_photo", field=models.TextField(), ), ] diff --git a/care/abdm/migrations/0004_auto_20221220_2325.py b/care/abdm/migrations/0004_auto_20221220_2325.py index 7e90626f80..613d539e15 100644 --- a/care/abdm/migrations/0004_auto_20221220_2325.py +++ b/care/abdm/migrations/0004_auto_20221220_2325.py @@ -4,50 +4,49 @@ class Migration(migrations.Migration): - dependencies = [ - ('abdm', '0003_auto_20221220_2321'), + ("abdm", "0003_auto_20221220_2321"), ] operations = [ migrations.AlterField( - model_name='abhanumber', - name='abha_number', + model_name="abhanumber", + name="abha_number", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='access_token', + model_name="abhanumber", + name="access_token", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='first_name', + model_name="abhanumber", + name="first_name", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='health_id', + model_name="abhanumber", + name="health_id", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='last_name', + model_name="abhanumber", + name="last_name", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='middle_name', + model_name="abhanumber", + name="middle_name", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='refresh_token', + model_name="abhanumber", + name="refresh_token", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='txn_id', + model_name="abhanumber", + name="txn_id", field=models.TextField(), ), ] diff --git a/care/abdm/migrations/0005_auto_20221220_2327.py b/care/abdm/migrations/0005_auto_20221220_2327.py index f9781a1dbf..1c0b9bc736 100644 --- a/care/abdm/migrations/0005_auto_20221220_2327.py +++ b/care/abdm/migrations/0005_auto_20221220_2327.py @@ -4,60 +4,59 @@ class Migration(migrations.Migration): - dependencies = [ - ('abdm', '0004_auto_20221220_2325'), + ("abdm", "0004_auto_20221220_2325"), ] operations = [ migrations.AlterField( - model_name='abhanumber', - name='abha_number', + model_name="abhanumber", + name="abha_number", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='access_token', + model_name="abhanumber", + name="access_token", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='email', + model_name="abhanumber", + name="email", field=models.EmailField(blank=True, max_length=254, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='first_name', + model_name="abhanumber", + name="first_name", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='health_id', + model_name="abhanumber", + name="health_id", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='last_name', + model_name="abhanumber", + name="last_name", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='middle_name', + model_name="abhanumber", + name="middle_name", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='profile_photo', + model_name="abhanumber", + name="profile_photo", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='refresh_token', + model_name="abhanumber", + name="refresh_token", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='txn_id', + model_name="abhanumber", + name="txn_id", field=models.TextField(blank=True, null=True), ), ] diff --git a/care/abdm/migrations/0006_auto_20230208_0915.py b/care/abdm/migrations/0006_auto_20230208_0915.py index ed00f037cd..940ed863c8 100644 --- a/care/abdm/migrations/0006_auto_20230208_0915.py +++ b/care/abdm/migrations/0006_auto_20230208_0915.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("abdm", "0005_auto_20221220_2327"), ] diff --git a/care/facility/migrations/0329_auto_20221219_1936.py b/care/facility/migrations/0329_auto_20221219_1936.py index c0b161de69..589981551d 100644 --- a/care/facility/migrations/0329_auto_20221219_1936.py +++ b/care/facility/migrations/0329_auto_20221219_1936.py @@ -7,20 +7,30 @@ class Migration(migrations.Migration): dependencies = [ - ('facility', '0328_merge_20221208_1110'), + ("facility", "0328_merge_20221208_1110"), ] operations = [ migrations.AlterField( - model_name='fileupload', - name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, - related_name='archived_by', to=settings.AUTH_USER_MODEL), + model_name="fileupload", + name="archived_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="archived_by", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='fileupload', - name='uploaded_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, - related_name='uploaded_by', to=settings.AUTH_USER_MODEL), + model_name="fileupload", + name="uploaded_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="uploaded_by", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/care/facility/migrations/0330_auto_20221220_2312.py b/care/facility/migrations/0330_auto_20221220_2312.py index 53e3038ed7..b0808c90ea 100644 --- a/care/facility/migrations/0330_auto_20221220_2312.py +++ b/care/facility/migrations/0330_auto_20221220_2312.py @@ -6,22 +6,31 @@ class Migration(migrations.Migration): dependencies = [ - ('abdm', '0002_auto_20221220_2312'), - ('facility', '0329_auto_20221219_1936'), + ("abdm", "0002_auto_20221220_2312"), + ("facility", "0329_auto_20221219_1936"), ] 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'), + 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'), + 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/migrations/0331_auto_20230130_1652.py b/care/facility/migrations/0331_auto_20230130_1652.py index 7b96181775..334743b8c0 100644 --- a/care/facility/migrations/0331_auto_20230130_1652.py +++ b/care/facility/migrations/0331_auto_20230130_1652.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('facility', '0330_auto_20221220_2312'), + ("facility", "0330_auto_20221220_2312"), ] operations = [ migrations.AlterField( - model_name='patientsearch', - name='state_id', + model_name="patientsearch", + name="state_id", field=models.IntegerField(null=True), ), ] diff --git a/care/facility/migrations/0344_merge_20230413_1120.py b/care/facility/migrations/0344_merge_20230413_1120.py index 96acc3ff38..79bc41b1d5 100644 --- a/care/facility/migrations/0344_merge_20230413_1120.py +++ b/care/facility/migrations/0344_merge_20230413_1120.py @@ -4,11 +4,9 @@ class Migration(migrations.Migration): - dependencies = [ - ('facility', '0343_auto_20230407_1850'), - ('facility', '0331_auto_20230130_1652'), + ("facility", "0343_auto_20230407_1850"), + ("facility", "0331_auto_20230130_1652"), ] - operations = [ - ] + operations = [] From c580e4b77c594bb997abef569e3ffe4cbc3a8025 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 21 Jun 2023 08:42:19 +0530 Subject: [PATCH 105/137] set debug to True --- config/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings/base.py b/config/settings/base.py index 18b50651bf..e8f38e7732 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -29,7 +29,7 @@ # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = env.bool("DJANGO_DEBUG", False) +DEBUG = env.bool("DJANGO_DEBUG", True) # Local time zone. Choices are # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. From 87d5e583386363cef73048aa7123ab2aa7b28d2f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 21 Jun 2023 10:13:38 +0530 Subject: [PATCH 106/137] added check_and_generate_mobile_otp --- care/abdm/api/viewsets/healthid.py | 2 +- care/abdm/utils/api_call.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 9a75bcda5e..dc7f191e8d 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -626,7 +626,7 @@ def confirm_with_demographics(self, request): ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( - # /v1/registration/aadhaar/checkAndGenerateMobileOTP + # /v2/registration/aadhaar/checkAndGenerateMobileOTP operation_id="check_and_generate_mobile_otp", request_body=GenerateMobileOtpRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 201bc47854..a84e5ff258 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -161,8 +161,13 @@ def verify_aadhaar_otp(self, data): 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 = "/v1/registration/aadhaar/generateMobileOTP" + path = "/v2/registration/aadhaar/generateMobileOTP" response = self.api.post(path, data) return response.json() From bffc433ff3f919dbb5c333efc760c44e6c88c67c Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 22 Jun 2023 09:11:45 +0530 Subject: [PATCH 107/137] added validation in link_via_qr --- care/abdm/api/viewsets/healthid.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index dc7f191e8d..0ee8287f7e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -302,13 +302,13 @@ def link_via_qr(self, request): 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, - # ) + if patient: + return Response( + { + "message": "A patient is already associated with the provided Abha Number" + }, + status=status.HTTP_400_BAD_REQUEST, + ) if ( "facilityId" not in data From ea1167fadb850bbb93f3e1609fc0852ad0d3a817 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 22 Jun 2023 09:27:31 +0530 Subject: [PATCH 108/137] fixed a typo in link_via_qr --- care/abdm/api/viewsets/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 0ee8287f7e..b05fe53074 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -287,7 +287,7 @@ def search_by_health_id(self, request): def link_via_qr(self, request): data = request.data - if ratelimit(request, "link_via_qr", [data["hdin"]], increment=False): + 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, @@ -320,7 +320,7 @@ def link_via_qr(self, request): ) if not HealthIdGateway().verify_demographics( - data["phr"] or data["hdin"], + data["phr"] or data["hidn"], data["name"], data["gender"], str(dob.year), From e623cd465b311e5c7947e4e51c820c9edcefd3e6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 22 Jun 2023 10:23:09 +0530 Subject: [PATCH 109/137] added a patientId is None validation in link_via_qr --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index b05fe53074..8b97de5d11 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -298,7 +298,7 @@ def link_via_qr(self, request): dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() - if "patientId" not in data: + if "patientId" not in data or data["patientId"] is None: patient = PatientRegistration.objects.filter( abha_number__abha_number=data["hidn"] ).first() From 6e9a40b7f118f3599120c35b55dda07474013eb3 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 22 Jun 2023 12:44:20 +0530 Subject: [PATCH 110/137] return error message in create_health_id --- care/abdm/api/viewsets/healthid.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 8b97de5d11..f80e8d5270 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -219,6 +219,12 @@ def create_health_id(self, request): 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, From 89d3cebdca02b8fc6e4d64697cb1c47ff7b58d88 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 07:53:32 +0530 Subject: [PATCH 111/137] added direct auth --- care/abdm/api/viewsets/auth.py | 19 +++++++++++++++++++ care/abdm/api/viewsets/healthid.py | 9 +++++---- care/abdm/utils/api_call.py | 24 +++++++++++++++++++++++- config/urls.py | 6 ++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 892b608e01..4b8e49389e 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -72,6 +72,25 @@ def post(self, request, *args, **kwargs): return Response({}, status=status.HTTP_202_ACCEPTED) +class AuthNotifyView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [ABDMAuthentication] + + def post(self, request, *args, **kwargs): + data = request.data + print(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 = (AllowAny,) authentication_classes = [ABDMAuthentication] diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index f80e8d5270..e3d0b33711 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -447,10 +447,11 @@ def add_care_context(self, request, *args, **kwargs): AbdmGateway().fetch_modes( { "healthId": consultation.patient.abha_number.abha_number, - "name": consultation.patient.abha_number.name, - "gender": consultation.patient.abha_number.gender, - "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), - "consultationId": consultation_id, + # "name": consultation.patient.abha_number.name, + # "gender": consultation.patient.abha_number.gender, + # "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + # "consultationId": consultation_id, + "authMode": "DIRECT", "purpose": "LINK", } ) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index a84e5ff258..3b5a9523c4 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -391,6 +391,10 @@ def fetch_modes(self, data): """ self.temp_memory[request_id] = data + if "authMode" in data and data["authMode"] == "DIRECT": + self.init(request_id) + return + payload = { "requestId": request_id, "timestamp": str( @@ -426,7 +430,7 @@ def init(self, prev_request_id): "query": { "id": data["healthId"], "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", - "authMode": "DEMOGRAPHICS", + "authMode": data["authMode"] if "authMode" in data else "DEMOGRAPHICS", "requester": {"type": "HIP", "id": self.hip_id}, }, } @@ -467,6 +471,24 @@ def confirm(self, transaction_id, prev_request_id): 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": str( + 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" diff --git a/config/urls.py b/config/urls.py index 0e325edc4a..0d891e1a37 100644 --- a/config/urls.py +++ b/config/urls.py @@ -10,6 +10,7 @@ from rest_framework_simplejwt.views import TokenVerifyView from care.abdm.api.viewsets.auth import ( + AuthNotifyView, DiscoverView, LinkConfirmView, LinkInitView, @@ -102,6 +103,11 @@ 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(), From 24dd683c46301e835687b4fb6bd0740db1c39612 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:02:22 +0530 Subject: [PATCH 112/137] resolve dependency issue in fhir --- requirements/base.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/base.txt b/requirements/base.txt index bff77ed5ab..c3a351f815 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -70,5 +70,6 @@ pycryptodome==3.16.0 pycryptodomex==3.16.0 # HCX fhir.resources==6.5.0 +pydantic==1.* jwcrypto==1.4.2 requests==2.31.0 From 6f91d09aeb1f06e1daf653a1ebe6f1880d4e28b1 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:33:11 +0530 Subject: [PATCH 113/137] debug --- care/abdm/utils/api_call.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 3b5a9523c4..8ef9c39e66 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -395,6 +395,8 @@ def fetch_modes(self, data): self.init(request_id) return + print("auth-init", data) + payload = { "requestId": request_id, "timestamp": str( From a0ddbb03f8fb6a04112fe9b0ca34e1c092cca3ee Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:36:41 +0530 Subject: [PATCH 114/137] debug --- care/abdm/utils/api_call.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 8ef9c39e66..ca5b24b218 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -395,8 +395,6 @@ def fetch_modes(self, data): self.init(request_id) return - print("auth-init", data) - payload = { "requestId": request_id, "timestamp": str( @@ -424,6 +422,8 @@ def init(self, prev_request_id): data = self.temp_memory[prev_request_id] self.temp_memory[request_id] = data + print("auth-init", data) + payload = { "requestId": request_id, "timestamp": str( From 6095d2c52cf825d5781c9ab57492ffb6f21b8ff5 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:44:54 +0530 Subject: [PATCH 115/137] send health id for direct auth --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index e3d0b33711..abeb91de21 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -446,7 +446,7 @@ def add_care_context(self, request, *args, **kwargs): AbdmGateway().fetch_modes( { - "healthId": consultation.patient.abha_number.abha_number, + "healthId": consultation.patient.abha_number.health_id, # "name": consultation.patient.abha_number.name, # "gender": consultation.patient.abha_number.gender, # "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), From d7ded21a1f41deab6d3d71def3c534944abda6b5 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:55:41 +0530 Subject: [PATCH 116/137] fix error in oninit on direct auth --- care/abdm/api/viewsets/auth.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 4b8e49389e..80f98ef537 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -34,7 +34,12 @@ class OnInitView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data print("on-init", data) - AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) + + if data["auth"]["transactionId"]["mode"] != "DIRECT": + AbdmGateway().confirm( + data["auth"]["transactionId"], data["resp"]["requestId"] + ) + return Response({}, status=status.HTTP_202_ACCEPTED) @@ -78,7 +83,7 @@ class AuthNotifyView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print(data) + print("auth-notify", data) if data["auth"]["status"] != "GRANTED": return From def4b39927fd160e863296f99f3ef4ad6c323d98 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 09:02:27 +0530 Subject: [PATCH 117/137] fixed a typo --- care/abdm/api/viewsets/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 80f98ef537..2903b6d73a 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -35,7 +35,7 @@ def post(self, request, *args, **kwargs): data = request.data print("on-init", data) - if data["auth"]["transactionId"]["mode"] != "DIRECT": + if data["auth"]["mode"] != "DIRECT": AbdmGateway().confirm( data["auth"]["transactionId"], data["resp"]["requestId"] ) From fa3075826cd1473038dd704f9386646ae8f4fd1d Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 09:10:39 +0530 Subject: [PATCH 118/137] do confirm to direct auth --- care/abdm/api/viewsets/auth.py | 5 +---- care/abdm/api/viewsets/healthid.py | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 2903b6d73a..d0ee48d36d 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -35,10 +35,7 @@ def post(self, request, *args, **kwargs): data = request.data print("on-init", data) - if data["auth"]["mode"] != "DIRECT": - AbdmGateway().confirm( - data["auth"]["transactionId"], data["resp"]["requestId"] - ) + AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index abeb91de21..b8a242c00b 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -447,10 +447,10 @@ def add_care_context(self, request, *args, **kwargs): AbdmGateway().fetch_modes( { "healthId": consultation.patient.abha_number.health_id, - # "name": consultation.patient.abha_number.name, - # "gender": consultation.patient.abha_number.gender, - # "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), - # "consultationId": consultation_id, + "name": consultation.patient.abha_number.name, + "gender": consultation.patient.abha_number.gender, + "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + "consultationId": consultation_id, "authMode": "DIRECT", "purpose": "LINK", } From 767c419ad319c92eb8dbdb73663230b97113295e Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 09:23:38 +0530 Subject: [PATCH 119/137] reverted back to demographics --- care/abdm/api/viewsets/healthid.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index b8a242c00b..ca06b938c7 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -447,11 +447,17 @@ def add_care_context(self, request, *args, **kwargs): AbdmGateway().fetch_modes( { "healthId": consultation.patient.abha_number.health_id, - "name": consultation.patient.abha_number.name, - "gender": consultation.patient.abha_number.gender, - "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + "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", + # "authMode": "DIRECT", "purpose": "LINK", } ) From a94442c70bf0a81825693117db89ad670d0a0aa3 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 11:03:31 +0530 Subject: [PATCH 120/137] send healthId --- care/abdm/utils/api_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index ca5b24b218..f544410c33 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -181,7 +181,7 @@ def verify_mobile_otp(self, data): def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" print("Creating Health ID with data: {}".format(data)) - data.pop("healthId", None) + # data.pop("healthId", None) response = self.api.post(path, data) return response.json() From 30ddd72b1411bf0e779678a5f783e0e9cf1c9440 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 12:24:15 +0530 Subject: [PATCH 121/137] remove abha address --- care/abdm/utils/api_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index f544410c33..ca5b24b218 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -181,7 +181,7 @@ def verify_mobile_otp(self, data): def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" print("Creating Health ID with data: {}".format(data)) - # data.pop("healthId", None) + data.pop("healthId", None) response = self.api.post(path, data) return response.json() From 92a4548f3e9c1cb5ba98935b6e3b3917dc03520c Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 13:49:09 +0530 Subject: [PATCH 122/137] debug data --- care/abdm/api/viewsets/auth.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index d0ee48d36d..87c16ca750 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -335,6 +335,10 @@ def post(self, request, *args, **kwargs): } ) + print("______________________________________________") + print(consent["notification"]["consentDetail"]["careContexts"][:-2:-1]) + print("______________________________________________") + AbdmGateway().data_notify( { "consent_id": data["hiRequest"]["consent"]["id"], From c7c326d70e5d10435f390ff69a7a79e795c32b29 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 14:09:13 +0530 Subject: [PATCH 123/137] hardcode consultation id for abdm data --- care/abdm/api/viewsets/auth.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 87c16ca750..7ca0af9519 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -305,9 +305,8 @@ def post(self, request, *args, **kwargs): "data": cipher.encrypt( Fhir( PatientConsultation.objects.get( - external_id=context[ - "careContextReference" - ] + external_id="6ddf9a81-e6f6-47c2-a2d0-63f64f66fcfd" + or context["careContextReference"] ) ).create_record(record) )["data"], From 70ad08b655183b24c4439e1d9c8cc8d3f22f61ce Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 14:18:07 +0530 Subject: [PATCH 124/137] removed hard coding --- care/abdm/api/viewsets/auth.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7ca0af9519..87c16ca750 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -305,8 +305,9 @@ def post(self, request, *args, **kwargs): "data": cipher.encrypt( Fhir( PatientConsultation.objects.get( - external_id="6ddf9a81-e6f6-47c2-a2d0-63f64f66fcfd" - or context["careContextReference"] + external_id=context[ + "careContextReference" + ] ) ).create_record(record) )["data"], From 0be66c97f63f18e338cd44bb23a9a7b7440bb7d7 Mon Sep 17 00:00:00 2001 From: Mathew Date: Tue, 4 Jul 2023 17:53:48 +0530 Subject: [PATCH 125/137] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 33de64a0b2..8881bd107a 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -28,7 +28,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} flavor: | - latest=true + latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 From b96cf47a06cd500d03503346f99ef72a22d126e4 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 5 Jul 2023 09:37:29 +0530 Subject: [PATCH 126/137] added support for abha card --- care/abdm/api/viewsets/healthid.py | 23 +++++++++++++++++++++++ care/abdm/utils/api_call.py | 7 +++++++ 2 files changed, 30 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index ca06b938c7..68fed4e250 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -282,6 +282,29 @@ def search_by_health_id(self, request): 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"}) + + response = HealthIdGateway().get_abha_card_png( + {"refreshToken": patient.abha_number.refresh_token} + ) + return Response(response, status=status.HTTP_200_OK) + @swagger_auto_schema( # /v1/registration/aadhaar/searchByHealthId operation_id="link_via_qr", diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index ca5b24b218..5124f44c6a 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -286,6 +286,13 @@ def get_profile(self, 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 response.json() + # /v1/account/qrCode def get_qr_code(self, data, auth): path = "/v1/account/qrCode" From 1ffe7b8daa633f8657680d367176b393f03bccd6 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 5 Jul 2023 10:11:27 +0530 Subject: [PATCH 127/137] store refresh token while abha creation --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 68fed4e250..2f1c2c6164 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -231,7 +231,7 @@ def create_health_id(self, request): { "txn_id": data["txnId"], "access_token": abha_profile["token"], - "refresh_token": None, + "refresh_token": abha_profile["refreshToken"], }, ) From 5af1584256ea0091e6fa36685b25fe6f2f8a871f Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 5 Jul 2023 10:21:20 +0530 Subject: [PATCH 128/137] get_abha_card_png return original response --- care/abdm/utils/api_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 5124f44c6a..c041f051bc 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -291,7 +291,7 @@ 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 response.json() + return response # /v1/account/qrCode def get_qr_code(self, data, auth): From 5205e18cc39517b0a1169b0397678f1ee38ae96b Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 5 Jul 2023 07:00:55 +0000 Subject: [PATCH 129/137] added pdf and png downloads for abha card --- care/abdm/api/viewsets/healthid.py | 13 ++++++++++--- care/abdm/utils/api_call.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 2f1c2c6164..7f29fd85a3 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -300,10 +300,17 @@ def get_abha_card(self, request): if not patient.abha_number: raise ValidationError({"abha": "Patient hasn't linked thier abha"}) - response = HealthIdGateway().get_abha_card_png( - {"refreshToken": patient.abha_number.refresh_token} - ) + 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) + @swagger_auto_schema( # /v1/registration/aadhaar/searchByHealthId diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index c041f051bc..53ce22b426 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -291,7 +291,16 @@ 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 response + + 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): From 53f9586b60c3addf14d5eb7eb96aba2011d80884 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 5 Jul 2023 07:23:23 +0000 Subject: [PATCH 130/137] send 401 when consent is denied in data request --- care/abdm/api/viewsets/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 87c16ca750..7d9305ec01 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -257,8 +257,8 @@ def post(self, request, *args, **kwargs): # TODO: uncomment later 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) + 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( From 42ed78889ca86e42180a5a813040354e043684ee Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 5 Jul 2023 09:39:07 +0000 Subject: [PATCH 131/137] Trigger Build From 59a1674c765aed8b01d25e6d2f82d1cf2d91cf70 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 08:12:51 +0530 Subject: [PATCH 132/137] send health id to createHealthIdWithPreVerified --- care/abdm/utils/api_call.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 53ce22b426..6fcc8c244a 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -181,7 +181,7 @@ def verify_mobile_otp(self, data): def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" print("Creating Health ID with data: {}".format(data)) - data.pop("healthId", None) + # data.pop("healthId", None) response = self.api.post(path, data) return response.json() @@ -291,16 +291,15 @@ 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) + return b64encode(response.content) # /v1/account/qrCode def get_qr_code(self, data, auth): From 7066101c2cfcb0bcd8901a0d7bbd5a94cbbf7971 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 11:46:39 +0530 Subject: [PATCH 133/137] modified link_via_qr to create abha_number --- care/abdm/api/viewsets/healthid.py | 99 +++++++++--------------------- 1 file changed, 28 insertions(+), 71 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 7f29fd85a3..2b4b15eb74 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -11,6 +11,7 @@ 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, @@ -25,7 +26,6 @@ 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.facility import Facility from care.facility.models.patient import PatientConsultation, PatientRegistration from care.utils.queryset.patient import get_patient_queryset from config.auth_views import CaptchaRequiredException @@ -307,10 +307,9 @@ def get_abha_card(self, request): return Response(response, status=status.HTTP_200_OK) response = HealthIdGateway().get_abha_card_pdf( - {"refreshToken": patient.abha_number.refresh_token} - ) + {"refreshToken": patient.abha_number.refresh_token} + ) return Response(response, status=status.HTTP_200_OK) - @swagger_auto_schema( # /v1/registration/aadhaar/searchByHealthId @@ -334,70 +333,16 @@ def link_via_qr(self, request): dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() - if "patientId" not in data or data["patientId"] is None: - 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, - ) - - if ( - "facilityId" not in data - or not Facility.objects.filter(external_id=data["facilityId"]).first() - ): - return Response( - {"message": "Enter a valid facilityId"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not HealthIdGateway().verify_demographics( - data["phr"] or data["hidn"], - data["name"], - data["gender"], - str(dob.year), - ): - return Response( - {"message": "Please enter valid data"}, - status=status.HTTP_403_FORBIDDEN, - ) - - patient = PatientRegistration.objects.create( - facility=Facility.objects.get(external_id=data["facilityId"]), - name=data["name"], - gender=1 - if data["gender"] == "M" - else 2 - if data["gender"] == "F" - else 3, - is_antenatal=False, - phone_number=data["mobile"], - emergency_phone_number=data["mobile"], - date_of_birth=dob, - blood_group="UNK", - nationality="India", - address=data["address"], - pincode=None, - created_by=None, - state=None, - district=None, - local_body=None, - ward=None, + 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, ) - else: - 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, - ) abha_number = AbhaNumber.objects.create( abha_number=data["hidn"], @@ -411,8 +356,6 @@ def link_via_qr(self, request): ) abha_number.save() - patient.abha_number = abha_number - patient.save() AbdmGateway().fetch_modes( { @@ -423,8 +366,22 @@ def link_via_qr(self, request): } ) - patient_serialized = PatientDetailSerializer(patient).data - return Response(patient_serialized, status=status.HTTP_200_OK) + if "patientId" in data or 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(abha_serialized, status=status.HTTP_200_OK) @swagger_auto_schema( operation_id="get_new_linking_token", From ff1c30060b9addf71d162edd12b46caf1d2bffe3 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 12:20:49 +0530 Subject: [PATCH 134/137] return an existing abha number if available in link_via_qr --- care/abdm/api/viewsets/healthid.py | 45 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 2b4b15eb74..559bfda44a 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -344,29 +344,34 @@ def link_via_qr(self, request): status=status.HTTP_400_BAD_REQUEST, ) - 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 = 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() + 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], - } - ) + 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 or data["patientId"] is not None: + if "patientId" in data and data["patientId"] is not None: patient = PatientRegistration.objects.filter( external_id=data["patientId"] ).first() From 869966cb1d0b8e9c0abf0cd585158bd81c443510 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 12:30:35 +0530 Subject: [PATCH 135/137] changed response data in link_via_qr --- care/abdm/api/viewsets/healthid.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 559bfda44a..4278289f16 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -386,7 +386,10 @@ def link_via_qr(self, request): patient.save() abha_serialized = AbhaNumberSerializer(abha_number).data - return Response(abha_serialized, status=status.HTTP_200_OK) + return Response( + {"id": abha_serialized["external_id"], "abha_profile": abha_serialized}, + status=status.HTTP_200_OK, + ) @swagger_auto_schema( operation_id="get_new_linking_token", From e0a1a59a51b2c9d51a5c69257f10f2fdd20220e4 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 12:53:43 +0530 Subject: [PATCH 136/137] changed discover filter logic --- care/abdm/api/viewsets/auth.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7d9305ec01..b8765b726c 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -109,6 +109,7 @@ class DiscoverView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data + print(data) patients = PatientRegistration.objects.all() verified_identifiers = data["patient"]["verifiedIdentifiers"] @@ -139,11 +140,8 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - patient = patients.filter( - abha_number__name=data["patient"]["name"], - abha_number__gender=data["patient"]["gender"], - # TODO: check date also - ).last() + # TODO: also filter by demographics + patient = patients.last() if not patient: return Response( From 3605b8b314a929c68714c879b8b93d9190e3f9d9 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 13:21:49 +0530 Subject: [PATCH 137/137] fixed searching logic in discover --- care/abdm/api/viewsets/auth.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index b8765b726c..7b94a9b9b0 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -121,11 +121,14 @@ def post(self, request, *args, **kwargs): ) 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{identifier['value']}") - | Q(phone_number=identifier["value"]) + Q(phone_number=f"+91{mobile}") | Q(phone_number=mobile) ) if identifier["type"] == "NDHM_HEALTH_NUMBER":