From 59236009ff2285b376aa9e60e80a228db396f7d4 Mon Sep 17 00:00:00 2001 From: Sravan Reddy Date: Thu, 7 Nov 2024 21:15:43 +0530 Subject: [PATCH] Capture payment phone numbers --- connectid/settings.py | 1 + payments/__init__.py | 0 payments/apps.py | 6 +++ payments/migrations/0001_initial.py | 31 +++++++++++++ payments/migrations/__init__.py | 0 payments/models.py | 21 +++++++++ payments/views.py | 71 +++++++++++++++++++++++++++++ users/urls.py | 4 ++ users/views.py | 32 ++++++++++--- 9 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 payments/__init__.py create mode 100644 payments/apps.py create mode 100644 payments/migrations/0001_initial.py create mode 100644 payments/migrations/__init__.py create mode 100644 payments/models.py create mode 100644 payments/views.py diff --git a/connectid/settings.py b/connectid/settings.py index a02f993..22ee62e 100644 --- a/connectid/settings.py +++ b/connectid/settings.py @@ -35,6 +35,7 @@ 'users.apps.UsersConfig', 'messaging', 'oauth2_provider', + 'payments', 'rest_framework', 'axes', 'fcm_django', diff --git a/payments/__init__.py b/payments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payments/apps.py b/payments/apps.py new file mode 100644 index 0000000..4886655 --- /dev/null +++ b/payments/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PaymentsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'payments' diff --git a/payments/migrations/0001_initial.py b/payments/migrations/0001_initial.py new file mode 100644 index 0000000..b5c68c4 --- /dev/null +++ b/payments/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2024-11-06 16:50 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='PaymentProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region=None)), + ('telecom_provider', models.CharField(max_length=50,blank=True, null=True)), + ('is_verified', models.BooleanField(default=False)), + ('is_validated', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment_profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/payments/migrations/__init__.py b/payments/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payments/models.py b/payments/models.py new file mode 100644 index 0000000..973d909 --- /dev/null +++ b/payments/models.py @@ -0,0 +1,21 @@ +from django.db import models + +from phonenumber_field.modelfields import PhoneNumberField +from users.models import ConnectUser + + +class PaymentProfile(models.Model): + user = models.OneToOneField( + ConnectUser, + on_delete=models.CASCADE, + related_name='payment_profile' + ) + phone_number = PhoneNumberField() + telecom_provider = models.CharField(max_length=50, blank=True, null=True) + # whether the number is verified using OTP + is_verified = models.BooleanField(default=False) + # whether the number is a valid payment receiver + is_validated = models.BooleanField(default=False) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) diff --git a/payments/views.py b/payments/views.py new file mode 100644 index 0000000..1538ef4 --- /dev/null +++ b/payments/views.py @@ -0,0 +1,71 @@ +from django.http import JsonResponse, HttpResponse, Http404 +from django.views.decorators.http import require_POST +from oauth2_provider.decorators import protected_resource +from utils.rest_framework import ClientProtectedResourceAuth +from rest_framework.decorators import api_view +from rest_framework.views import APIView +from users.models import ConnectUser, PhoneDevice +from utils.twilio import lookup_telecom_provider +from .models import PaymentProfile + + +@api_view(['POST']) +def update_payment_profile_phone(request): + user = request.user + phone_number = request.data.get('phone_number') + telecom_provider = lookup_telecom_provider(phone_number) + payment_profile, created = PaymentProfile.objects.update_or_create( + user=user, + defaults={ + 'phone_number': phone_number, + 'telecom_provider': telecom_provider, + 'is_verified': False, + 'is_validated': False + } + ) + return PhoneDevice.send_otp_httpresponse(phone_number=payment_profile.phone_number, user=payment_profile.user) + + +@api_view(['POST']) +def confirm_payment_profile_otp(request): + PaymentProfile.objects.get(user=request.user) + payment_profile = request.user.payment_profile + device = PhoneDevice.objects.get(phone_number=payment_profile.phone_number, user=payment_profile.user) + if not device.verify_token(request.data.get('token')): + return JsonResponse({"error": "OTP token is incorrect"}, status=401) + + payment_profile.is_verified = True + payment_profile.save() + return JsonResponse({"success": True}) + + +@require_POST +@protected_resource(scopes=[]) +def validate_payment_phone_number(request): + username = request.data["username"] + phone_number = request.data["phone_number"] + user = ConnectUser.objects.get(username=username) + profile = getattr(user, "payment_profile") + + if not profile or profile.phone_number != phone_number: + raise Http404("Payment number not found") + + profile.is_validated = True + return HttpResponse() + + +class ValidatePhoneNumber(APIView): + authentication_classes = [ClientProtectedResourceAuth] + + def post(self, request, *args, **kwargs): + username = request.data["username"] + phone_number = request.data["phone_number"] + user = ConnectUser.objects.get(username=username) + profile = getattr(user, "payment_profile") + + if not profile or profile.phone_number != phone_number: + raise Http404("Payment number not found") + + profile.is_validated = True + profile.save() + return HttpResponse() diff --git a/users/urls.py b/users/urls.py index b614f0c..bc34fde 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,6 +1,7 @@ from django.urls import path from . import views +from payments import views as payment_views urlpatterns = [ path('', views.test, name='test'), @@ -32,4 +33,7 @@ path('fetch_db_key', views.fetch_db_key, name='fetch_db_key'), path('recover/initiate_deactivation', views.initiate_deactivation, name='initiate_deactivation'), path('recover/confirm_deactivation', views.confirm_deactivation, name='confirm_deactivation'), + path('profile/payment_phone_number', payment_views.update_payment_profile_phone, name='update_payment_profile_phone'), + path('profile/confirm_payment_otp', payment_views.confirm_payment_profile_otp, name='confirm_payment_profile_otp'), + path('profile/validate_payment_phone_number', payment_views.ValidatePhoneNumber.as_view(), name='validate_payment_phone_number'), ] diff --git a/users/views.py b/users/views.py index 722a29b..90e440b 100644 --- a/users/views.py +++ b/users/views.py @@ -3,7 +3,7 @@ from django.contrib.auth.hashers import check_password from django.contrib.auth.password_validation import validate_password -from django.core.exceptions import ValidationError +from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.http import HttpResponse, JsonResponse from django.utils.timezone import now from django.views import View @@ -180,7 +180,9 @@ def confirm_secondary_recovery_otp(request): status.step = RecoveryStatus.RecoverySteps.RESET_PASSWORD status.save() db_key = UserKey.get_or_create_key_for_user(user) - return JsonResponse({"name": user.name, "username": user.username, "db_key": db_key.key}) + user_data = {"name": user.name, "username": user.username, "db_key": db_key.key} + user_data.update(user_payment_profile(user)) + return JsonResponse(user_data) @api_view(['POST']) @@ -199,8 +201,7 @@ def confirm_password(request): if not check_password(password, user.password): return HttpResponse(status=401) status.delete() - db_key = UserKey.get_or_create_key_for_user(user) - return JsonResponse({"name": user.name, "username": user.username, "secondary_phone_validate_by": user.recovery_phone_validation_deadline, "db_key": db_key.key}) + return JsonResponse(user_data(user)) @api_view(['POST']) @@ -305,6 +306,26 @@ def set_recovery_pin(request): return HttpResponse() +def user_data(user): + db_key = UserKey.get_or_create_key_for_user(user) + user_data = {"name": user.name, "username": user.username, "secondary_phone_validate_by": user.recovery_phone_validation_deadline, "db_key": db_key.key} + user_data.update(user_payment_profile(user)) + return user_data + + +def user_payment_profile(user): + try: + profile = user.payment_profile + return {"payment_profile": { + "phone_number": profile.phone_number, + "telecom_provider": profile.telecom_provider, + "is_verified": profile.is_verified, + "is_validated": profile.is_validated, + }} + except ObjectDoesNotExist: + return {} + + @api_view(['POST']) @permission_classes([]) def confirm_recovery_pin(request): @@ -322,8 +343,7 @@ def confirm_recovery_pin(request): return JsonResponse({"error": "Recovery PIN is incorrect"}, status=401) status.step = RecoveryStatus.RecoverySteps.RESET_PASSWORD status.save() - db_key = UserKey.get_or_create_key_for_user(user) - return JsonResponse({"name": user.name, "username": user.username, "secondary_phone_validate_by": user.recovery_phone_validation_deadline, "db_key": db_key.key}) + return JsonResponse(user_data(user)) @api_view(['GET'])