From b346f67ec3b3ff953cdc3818aae90dcbd2c3408b Mon Sep 17 00:00:00 2001 From: devchima Date: Thu, 19 Dec 2024 05:16:34 -0500 Subject: [PATCH 1/2] Create directory for turn alerts and model --- rp_turn_alerts/__init__.py | 0 rp_turn_alerts/admin.py | 5 + rp_turn_alerts/apps.py | 5 + rp_turn_alerts/migrations/__init__.py | 0 rp_turn_alerts/models.py | 22 +++ rp_turn_alerts/tests/__init__.py | 0 rp_turn_alerts/urls.py | 0 rp_turn_alerts/validators.py | 18 ++ rp_turn_alerts/views.py | 250 ++++++++++++++++++++++++++ 9 files changed, 300 insertions(+) create mode 100644 rp_turn_alerts/__init__.py create mode 100644 rp_turn_alerts/admin.py create mode 100644 rp_turn_alerts/apps.py create mode 100644 rp_turn_alerts/migrations/__init__.py create mode 100644 rp_turn_alerts/models.py create mode 100644 rp_turn_alerts/tests/__init__.py create mode 100644 rp_turn_alerts/urls.py create mode 100644 rp_turn_alerts/validators.py create mode 100644 rp_turn_alerts/views.py diff --git a/rp_turn_alerts/__init__.py b/rp_turn_alerts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rp_turn_alerts/admin.py b/rp_turn_alerts/admin.py new file mode 100644 index 0000000..f06f4ca --- /dev/null +++ b/rp_turn_alerts/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import TurnAlerts + +admin.site.register(TurnAlerts) diff --git a/rp_turn_alerts/apps.py b/rp_turn_alerts/apps.py new file mode 100644 index 0000000..15cf869 --- /dev/null +++ b/rp_turn_alerts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RpTurnAlertsConfig(AppConfig): + name = "rp_turn_alerts" diff --git a/rp_turn_alerts/migrations/__init__.py b/rp_turn_alerts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rp_turn_alerts/models.py b/rp_turn_alerts/models.py new file mode 100644 index 0000000..bbf8c0d --- /dev/null +++ b/rp_turn_alerts/models.py @@ -0,0 +1,22 @@ +from django.db import models + +from .validators import za_phone_number + + +class TurnAlerts(models.Model): + message_id = models.UUIDField() + msisdn = models.CharField(max_length=255, validators=[za_phone_number]) + message_status = models.TextField(null=True, blank=True) + message_direction = models.TextField(null=True, blank=True) + message_body = models.TextField(null=True, blank=True) + fallback_channel = models.TextField(null=True, blank=True) + message_type = models.TextField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return ( + f"Recipient_ID: {self.message_id} " + f"MSISDN: {self.msisdn} \n" + f"Created_at: {self.created_at} \n" + f"message_status: {self.message_status} \n" + ) diff --git a/rp_turn_alerts/tests/__init__.py b/rp_turn_alerts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rp_turn_alerts/urls.py b/rp_turn_alerts/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/rp_turn_alerts/validators.py b/rp_turn_alerts/validators.py new file mode 100644 index 0000000..780dd93 --- /dev/null +++ b/rp_turn_alerts/validators.py @@ -0,0 +1,18 @@ +import functools + +import phonenumbers +from rest_framework.serializers import ValidationError + + +def _phone_number(value, country): + try: + number = phonenumbers.parse(value, country) + except phonenumbers.NumberParseException as e: + raise ValidationError(str(e)) + if not phonenumbers.is_possible_number(number): + raise ValidationError("Not a possible phone number") + if not phonenumbers.is_valid_number(number): + raise ValidationError("Not a valid phone number") + + +za_phone_number = functools.partial(_phone_number, country="ZA") diff --git a/rp_turn_alerts/views.py b/rp_turn_alerts/views.py new file mode 100644 index 0000000..6311fbb --- /dev/null +++ b/rp_turn_alerts/views.py @@ -0,0 +1,250 @@ +from django.http import JsonResponse +from rest_framework import status +from rest_framework.views import APIView + +from sidekick.models import Organization +from sidekick.utils import clean_msisdn + +from .models import MsisdnInformation, TopupAttempt +from .tasks import buy_airtime_take_action, buy_product_take_action, topup_data + + +def process_status_code(info): + """ + returns a JsonResponse object with status updated to reflect info + + For more detail on possible TransferTo error codes, see + section "9.0 Standard API Errors" in https://shop.transferto.com/shop/v3/doc/TransferTo_API.pdf + + @param info: dict containing key "error_code" + @returns: JsonResponse object with status updated to reflect "error_code" + @raises keyError: if "error_code" is not contained within info dict + """ + error_code = info["error_code"] + if error_code not in ["0", 0]: + return JsonResponse(info, status=400) + # default to 200 status code + return JsonResponse(info) + + +class TransferToView(APIView): + client_method_name = None + args_for_client_method = None + + def get(self, request, *args, **kwargs): + # check that org exists + # check that request belongs to org + # check that org has TransferTo account + org_id = kwargs["org_id"] + + try: + org = Organization.objects.get(id=org_id) + except Organization.DoesNotExist: + return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) + + if not org.users.filter(id=request.user.id).exists(): + return JsonResponse(data={}, status=status.HTTP_401_UNAUTHORIZED) + + try: + client = org.transferto_account.first().get_transferto_client() + except AttributeError: + return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) + + if self.args_for_client_method: + kwargs_for_client = { + key: kwargs[key] for key in self.args_for_client_method + } + response = getattr(client, self.client_method_name)(**kwargs_for_client) + else: + response = getattr(client, self.client_method_name)() + if "error_code" in response: + return process_status_code(response) + return JsonResponse(response) + + +class Ping(TransferToView): + client_method_name = "ping" + + +class MsisdnInfo(APIView): + def get(self, request, *args, **kwargs): + org_id = kwargs["org_id"] + msisdn = kwargs["msisdn"] + + try: + org = Organization.objects.get(id=org_id) + except Organization.DoesNotExist: + return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) + + if not org.users.filter(id=request.user.id).exists(): + return JsonResponse(data={}, status=status.HTTP_401_UNAUTHORIZED) + + use_cache = ( + request.GET.get("no_cache", False) + and request.GET.get("no_cache").lower() == "true" + ) + if ( + use_cache + or not MsisdnInformation.objects.filter( + msisdn=clean_msisdn(msisdn) + ).exists() + ): + try: + client = org.transferto_account.first().get_transferto_client() + except AttributeError: + return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) + + cleaned_msisdn = clean_msisdn(msisdn) + info = client.get_misisdn_info(cleaned_msisdn) + MsisdnInformation.objects.create(data=info, msisdn=cleaned_msisdn) + # get cached result + else: + info = dict( + MsisdnInformation.objects.filter(msisdn=clean_msisdn(msisdn)) + .latest() + .data + ) + return process_status_code(info) + + +class ReserveId(TransferToView): + client_method_name = "reserve_id" + + +class GetCountries(TransferToView): + client_method_name = "get_countries" + + +class GetOperators(TransferToView): + client_method_name = "get_operators" + args_for_client_method = ["country_id"] + + +class GetOperatorAirtimeProducts(TransferToView): + client_method_name = "get_operator_airtime_products" + args_for_client_method = ["operator_id"] + + +class GetOperatorProducts(TransferToView): + client_method_name = "get_operator_products" + args_for_client_method = ["operator_id"] + + +class GetCountryServices(TransferToView): + client_method_name = "get_country_services" + args_for_client_method = ["country_id"] + + +class TopUpData(APIView): + def get(self, request, *args, **kwargs): + org_id = kwargs["org_id"] + + try: + org = Organization.objects.get(id=org_id) + except Organization.DoesNotExist: + return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) + + if not org.users.filter(id=request.user.id).exists(): + return JsonResponse(data={}, status=status.HTTP_401_UNAUTHORIZED) + + try: + # check that there is a valid TransferTo Account attached + org.transferto_account.first().get_transferto_client() + except AttributeError: + return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) + + data = request.GET.dict() + msisdn = data["msisdn"] + user_uuid = data["user_uuid"] + data_amount = data["data_amount"] + + # org_id, msisdn, user_uuid, amount + # e.g. 1, "+27827620000", "4a1b8cc8-905c-4c44-8bd2-dee3c4a3e2d1", "100MB" + topup_data.delay(org_id, msisdn, user_uuid, data_amount) + + return JsonResponse({"info_txt": "top_up_data"}) + + +class BuyProductTakeAction(APIView): + def get(self, request, *args, **kwargs): + org_id = kwargs["org_id"] + product_id = kwargs["product_id"] + msisdn = kwargs["msisdn"] + + flow_uuid_key = "flow_uuid" + user_uuid_key = "user_uuid" + data = dict(request.GET.dict()) + + flow_start = request.GET.get(flow_uuid_key, False) + if flow_start: + del data[flow_uuid_key] + user_uuid = request.GET.get(user_uuid_key, False) + if user_uuid: + del data[user_uuid_key] + # remaining variables will be coerced from key:value mapping + # which represents variable on rapidpro to update: variable from response + + buy_product_take_action.delay( + org_id, + clean_msisdn(msisdn), + product_id, + user_uuid=user_uuid, + values_to_update=data, + flow_start=flow_start, + ) + return JsonResponse({"info_txt": "buy_product_take_action"}) + + +class BuyAirtimeTakeAction(APIView): + def get(self, request, *args, **kwargs): + org_id = kwargs["org_id"] + airtime_amount = kwargs["airtime_amount"] + msisdn = kwargs["msisdn"] + from_string = kwargs["from_string"] + + try: + org = Organization.objects.get(id=org_id) + except Organization.DoesNotExist: + return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) + + if not org.users.filter(id=request.user.id).exists(): + return JsonResponse(data={}, status=status.HTTP_401_UNAUTHORIZED) + + try: + # check that there is a valid TransferTo Account attached + org.transferto_account.first().get_transferto_client() + except AttributeError: + return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) + + flow_uuid_key = "flow_uuid" + user_uuid_key = "user_uuid" + fail_flow_uuid_key = "fail_flow_start" + data = dict(request.GET.dict()) + + flow_start = request.GET.get(flow_uuid_key, False) + if flow_start: + del data[flow_uuid_key] + user_uuid = request.GET.get(user_uuid_key, False) + if user_uuid: + del data[user_uuid_key] + fail_flow_start = request.GET.get(fail_flow_uuid_key, False) + if fail_flow_start: + del data[fail_flow_uuid_key] + # remaining variables will be coerced from key:value mapping + # which represents variable on rapidpro to update: variable from response + + topup_attempt = TopupAttempt.objects.create( + msisdn=msisdn, + from_string=from_string, + amount=airtime_amount, + org=org, + rapidpro_user_uuid=user_uuid, + ) + + buy_airtime_take_action.delay( + topup_attempt_id=topup_attempt.id, + values_to_update=data, + flow_start=flow_start, + fail_flow_start=fail_flow_start, + ) + return JsonResponse({"info_txt": "buy_airtime_take_action"}) From da9f10a984ee45a8ad2fa07d58b85a636298d165 Mon Sep 17 00:00:00 2001 From: devchima Date: Thu, 19 Dec 2024 05:24:55 -0500 Subject: [PATCH 2/2] Clear views --- rp_turn_alerts/views.py | 250 ---------------------------------------- 1 file changed, 250 deletions(-) diff --git a/rp_turn_alerts/views.py b/rp_turn_alerts/views.py index 6311fbb..e69de29 100644 --- a/rp_turn_alerts/views.py +++ b/rp_turn_alerts/views.py @@ -1,250 +0,0 @@ -from django.http import JsonResponse -from rest_framework import status -from rest_framework.views import APIView - -from sidekick.models import Organization -from sidekick.utils import clean_msisdn - -from .models import MsisdnInformation, TopupAttempt -from .tasks import buy_airtime_take_action, buy_product_take_action, topup_data - - -def process_status_code(info): - """ - returns a JsonResponse object with status updated to reflect info - - For more detail on possible TransferTo error codes, see - section "9.0 Standard API Errors" in https://shop.transferto.com/shop/v3/doc/TransferTo_API.pdf - - @param info: dict containing key "error_code" - @returns: JsonResponse object with status updated to reflect "error_code" - @raises keyError: if "error_code" is not contained within info dict - """ - error_code = info["error_code"] - if error_code not in ["0", 0]: - return JsonResponse(info, status=400) - # default to 200 status code - return JsonResponse(info) - - -class TransferToView(APIView): - client_method_name = None - args_for_client_method = None - - def get(self, request, *args, **kwargs): - # check that org exists - # check that request belongs to org - # check that org has TransferTo account - org_id = kwargs["org_id"] - - try: - org = Organization.objects.get(id=org_id) - except Organization.DoesNotExist: - return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) - - if not org.users.filter(id=request.user.id).exists(): - return JsonResponse(data={}, status=status.HTTP_401_UNAUTHORIZED) - - try: - client = org.transferto_account.first().get_transferto_client() - except AttributeError: - return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) - - if self.args_for_client_method: - kwargs_for_client = { - key: kwargs[key] for key in self.args_for_client_method - } - response = getattr(client, self.client_method_name)(**kwargs_for_client) - else: - response = getattr(client, self.client_method_name)() - if "error_code" in response: - return process_status_code(response) - return JsonResponse(response) - - -class Ping(TransferToView): - client_method_name = "ping" - - -class MsisdnInfo(APIView): - def get(self, request, *args, **kwargs): - org_id = kwargs["org_id"] - msisdn = kwargs["msisdn"] - - try: - org = Organization.objects.get(id=org_id) - except Organization.DoesNotExist: - return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) - - if not org.users.filter(id=request.user.id).exists(): - return JsonResponse(data={}, status=status.HTTP_401_UNAUTHORIZED) - - use_cache = ( - request.GET.get("no_cache", False) - and request.GET.get("no_cache").lower() == "true" - ) - if ( - use_cache - or not MsisdnInformation.objects.filter( - msisdn=clean_msisdn(msisdn) - ).exists() - ): - try: - client = org.transferto_account.first().get_transferto_client() - except AttributeError: - return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) - - cleaned_msisdn = clean_msisdn(msisdn) - info = client.get_misisdn_info(cleaned_msisdn) - MsisdnInformation.objects.create(data=info, msisdn=cleaned_msisdn) - # get cached result - else: - info = dict( - MsisdnInformation.objects.filter(msisdn=clean_msisdn(msisdn)) - .latest() - .data - ) - return process_status_code(info) - - -class ReserveId(TransferToView): - client_method_name = "reserve_id" - - -class GetCountries(TransferToView): - client_method_name = "get_countries" - - -class GetOperators(TransferToView): - client_method_name = "get_operators" - args_for_client_method = ["country_id"] - - -class GetOperatorAirtimeProducts(TransferToView): - client_method_name = "get_operator_airtime_products" - args_for_client_method = ["operator_id"] - - -class GetOperatorProducts(TransferToView): - client_method_name = "get_operator_products" - args_for_client_method = ["operator_id"] - - -class GetCountryServices(TransferToView): - client_method_name = "get_country_services" - args_for_client_method = ["country_id"] - - -class TopUpData(APIView): - def get(self, request, *args, **kwargs): - org_id = kwargs["org_id"] - - try: - org = Organization.objects.get(id=org_id) - except Organization.DoesNotExist: - return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) - - if not org.users.filter(id=request.user.id).exists(): - return JsonResponse(data={}, status=status.HTTP_401_UNAUTHORIZED) - - try: - # check that there is a valid TransferTo Account attached - org.transferto_account.first().get_transferto_client() - except AttributeError: - return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) - - data = request.GET.dict() - msisdn = data["msisdn"] - user_uuid = data["user_uuid"] - data_amount = data["data_amount"] - - # org_id, msisdn, user_uuid, amount - # e.g. 1, "+27827620000", "4a1b8cc8-905c-4c44-8bd2-dee3c4a3e2d1", "100MB" - topup_data.delay(org_id, msisdn, user_uuid, data_amount) - - return JsonResponse({"info_txt": "top_up_data"}) - - -class BuyProductTakeAction(APIView): - def get(self, request, *args, **kwargs): - org_id = kwargs["org_id"] - product_id = kwargs["product_id"] - msisdn = kwargs["msisdn"] - - flow_uuid_key = "flow_uuid" - user_uuid_key = "user_uuid" - data = dict(request.GET.dict()) - - flow_start = request.GET.get(flow_uuid_key, False) - if flow_start: - del data[flow_uuid_key] - user_uuid = request.GET.get(user_uuid_key, False) - if user_uuid: - del data[user_uuid_key] - # remaining variables will be coerced from key:value mapping - # which represents variable on rapidpro to update: variable from response - - buy_product_take_action.delay( - org_id, - clean_msisdn(msisdn), - product_id, - user_uuid=user_uuid, - values_to_update=data, - flow_start=flow_start, - ) - return JsonResponse({"info_txt": "buy_product_take_action"}) - - -class BuyAirtimeTakeAction(APIView): - def get(self, request, *args, **kwargs): - org_id = kwargs["org_id"] - airtime_amount = kwargs["airtime_amount"] - msisdn = kwargs["msisdn"] - from_string = kwargs["from_string"] - - try: - org = Organization.objects.get(id=org_id) - except Organization.DoesNotExist: - return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) - - if not org.users.filter(id=request.user.id).exists(): - return JsonResponse(data={}, status=status.HTTP_401_UNAUTHORIZED) - - try: - # check that there is a valid TransferTo Account attached - org.transferto_account.first().get_transferto_client() - except AttributeError: - return JsonResponse(data={}, status=status.HTTP_400_BAD_REQUEST) - - flow_uuid_key = "flow_uuid" - user_uuid_key = "user_uuid" - fail_flow_uuid_key = "fail_flow_start" - data = dict(request.GET.dict()) - - flow_start = request.GET.get(flow_uuid_key, False) - if flow_start: - del data[flow_uuid_key] - user_uuid = request.GET.get(user_uuid_key, False) - if user_uuid: - del data[user_uuid_key] - fail_flow_start = request.GET.get(fail_flow_uuid_key, False) - if fail_flow_start: - del data[fail_flow_uuid_key] - # remaining variables will be coerced from key:value mapping - # which represents variable on rapidpro to update: variable from response - - topup_attempt = TopupAttempt.objects.create( - msisdn=msisdn, - from_string=from_string, - amount=airtime_amount, - org=org, - rapidpro_user_uuid=user_uuid, - ) - - buy_airtime_take_action.delay( - topup_attempt_id=topup_attempt.id, - values_to_update=data, - flow_start=flow_start, - fail_flow_start=fail_flow_start, - ) - return JsonResponse({"info_txt": "buy_airtime_take_action"})