feat: implement customer registration, profile management, and auth tests
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 35s

This commit is contained in:
vickytechkey 2026-11-01 20:53:36 +05:30
parent d32c653013
commit 4ca070ad8c
3 changed files with 257 additions and 60 deletions

View file

@ -5,18 +5,19 @@ from orders.models import CustomerProfile
class UserRegisterSerializer(serializers.ModelSerializer): class UserRegisterSerializer(serializers.ModelSerializer):
phone = serializers.CharField(write_only=True, required=False, allow_blank=True) phone = serializers.CharField(write_only=True, required=False, allow_blank=True)
address = serializers.CharField(write_only=True, required=False, allow_blank=True) address = serializers.CharField(write_only=True, required=False, allow_blank=True)
password = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True, required=False, default='TradhoxOtpUser2026!')
first_name = serializers.CharField(required=False, allow_blank=True) first_name = serializers.CharField(required=False, allow_blank=True)
last_name = serializers.CharField(required=False, allow_blank=True) last_name = serializers.CharField(required=False, allow_blank=True)
email = serializers.EmailField(required=False, allow_blank=True)
class Meta: class Meta:
model = User model = User
fields = ['username', 'email', 'password', 'first_name', 'last_name', 'phone', 'address'] fields = ['username', 'email', 'password', 'first_name', 'last_name', 'phone', 'address']
def create(self, validated_data): def create(self, validated_data):
phone = validated_data.pop('phone', '') phone = validated_data.pop('phone', '') or validated_data.get('username')
address = validated_data.pop('address', '') address = validated_data.pop('address', '')
password = validated_data.pop('password') password = validated_data.pop('password', 'TradhoxOtpUser2026!')
user = User.objects.create_user( user = User.objects.create_user(
username=validated_data['username'], username=validated_data['username'],
@ -25,5 +26,12 @@ class UserRegisterSerializer(serializers.ModelSerializer):
last_name=validated_data.get('last_name', ''), last_name=validated_data.get('last_name', ''),
password=password password=password
) )
CustomerProfile.objects.create(user=user, phone=phone, default_address=address) CustomerProfile.objects.create(
user=user,
phone=phone,
default_address=address,
gender='Prefer not to say',
wallet_balance=0.00,
loyalty_coins=200
)
return user return user

View file

@ -1,3 +1,124 @@
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework import status
from orders.models import CustomerProfile
# Create your tests here. class CustomerAuthAndProfileTests(TestCase):
def setUp(self):
self.client = APIClient()
self.test_phone = '9876543210'
self.test_user = User.objects.create_user(
username=self.test_phone,
first_name='Existing',
last_name='Patron',
email='patron@example.com',
password='TestPassword123!'
)
self.test_profile = CustomerProfile.objects.create(
user=self.test_user,
phone=self.test_phone,
default_address='123 Heritage Lane, Chennai',
wallet_balance=150.00,
loyalty_coins=250,
gender='Female'
)
def test_signup_successful(self):
new_phone = '9123456789'
payload = {
'phone': new_phone,
'full_name': 'New Artisan Patron',
'email': 'newpatron@tradhox.com',
'address': '45 Craft Colony, Kanchipuram'
}
response = self.client.post('/api/v1/customer/auth/register/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['username'], new_phone)
self.assertEqual(response.data['phone'], new_phone)
self.assertEqual(response.data['first_name'], 'New')
self.assertEqual(response.data['last_name'], 'Artisan Patron')
# Verify saved in database
saved_user = User.objects.get(username=new_phone)
self.assertEqual(saved_user.first_name, 'New')
self.assertEqual(saved_user.email, 'newpatron@tradhox.com')
self.assertEqual(saved_user.customer_profile.phone, new_phone)
self.assertEqual(saved_user.customer_profile.loyalty_coins, 200)
def test_signup_duplicate_phone_rejected(self):
# Attempt to register with already existing phone
payload = {
'phone': self.test_phone,
'full_name': 'Duplicate Person',
'email': 'duplicate@example.com'
}
response = self.client.post('/api/v1/customer/auth/register/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('already registered', response.data['error'].lower())
self.assertEqual(response.data['code'], 'already_registered')
def test_signup_invalid_phone_rejected(self):
payload = {
'phone': '12345', # Not 10 digits
'full_name': 'Invalid Phone User'
}
response = self.client.post('/api/v1/customer/auth/register/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('valid 10-digit', response.data['error'])
def test_get_user_profile_by_phone(self):
response = self.client.get(f'/api/v1/customer/profile/?phone={self.test_phone}')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['phone'], self.test_phone)
self.assertEqual(response.data['username'], self.test_phone)
self.assertEqual(response.data['first_name'], 'Existing')
self.assertEqual(response.data['last_name'], 'Patron')
self.assertEqual(response.data['full_name'], 'Existing Patron')
self.assertEqual(response.data['email'], 'patron@example.com')
self.assertEqual(response.data['gender'], 'Female')
self.assertEqual(response.data['wallet_balance'], 150.00)
self.assertEqual(response.data['loyalty_coins'], 250)
self.assertEqual(response.data['address'], '123 Heritage Lane, Chennai')
def test_put_user_profile_update(self):
update_payload = {
'phone': self.test_phone,
'full_name': 'Updated Name',
'email': 'updated@example.com',
'gender': 'Male',
'address': '789 Updated St, Coimbatore'
}
response = self.client.put('/api/v1/customer/profile/', update_payload, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['first_name'], 'Updated')
self.assertEqual(response.data['last_name'], 'Name')
self.assertEqual(response.data['email'], 'updated@example.com')
self.assertEqual(response.data['gender'], 'Male')
self.assertEqual(response.data['address'], '789 Updated St, Coimbatore')
# Verify DB persisted
self.test_user.refresh_from_db()
self.assertEqual(self.test_user.first_name, 'Updated')
self.assertEqual(self.test_user.email, 'updated@example.com')
self.assertEqual(self.test_user.customer_profile.gender, 'Male')
def test_send_otp_duplicate_register_fails(self):
payload = {
'mobile_number': self.test_phone,
'otp_code': '123456',
'purpose': 'register'
}
response = self.client.post('/api/v1/customer/auth/send-whatsapp/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('already exists', response.data['message'])
def test_send_otp_nonexistent_login_fails(self):
payload = {
'mobile_number': '9999900000',
'otp_code': '123456',
'purpose': 'login'
}
response = self.client.post('/api/v1/customer/auth/send-whatsapp/', payload, format='json')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('Account not found', response.data['message'])

View file

@ -8,6 +8,7 @@ from rest_framework import status, permissions
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.contrib.auth.models import User from django.contrib.auth.models import User
from orders.models import CustomerProfile
from .serializers import UserRegisterSerializer from .serializers import UserRegisterSerializer
audit_logger = logging.getLogger('audit') audit_logger = logging.getLogger('audit')
@ -16,60 +17,116 @@ class UserRegisterView(APIView):
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
def post(self, request): def post(self, request):
serializer = UserRegisterSerializer(data=request.data) raw_phone = request.data.get('phone') or request.data.get('username') or ''
clean_digits = ''.join(c for c in str(raw_phone) if c.isdigit())
phone_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits
if not phone_10 or len(phone_10) != 10:
return Response(
{"error": "A valid 10-digit mobile phone number is required."},
status=status.HTTP_400_BAD_REQUEST
)
# Check for duplicate registration using phone number as primary key
existing_user = (
User.objects.filter(username=phone_10).first()
or User.objects.filter(customer_profile__phone=phone_10).first()
)
if existing_user:
audit_logger.warning(f"AUDIT - Duplicate registration attempt for phone '{phone_10}'")
return Response(
{
"error": "This phone number is already registered. Please sign in instead.",
"code": "already_registered",
"phone": phone_10
},
status=status.HTTP_400_BAD_REQUEST
)
# Prepare normalized registration data
req_data = request.data.copy()
req_data['username'] = phone_10
req_data['phone'] = phone_10
if not req_data.get('password'):
req_data['password'] = 'TradhoxOtpUser2026!'
if 'full_name' in req_data and not req_data.get('first_name'):
parts = str(req_data['full_name']).strip().split()
req_data['first_name'] = parts[0] if parts else ''
req_data['last_name'] = ' '.join(parts[1:]) if len(parts) > 1 else ''
elif 'name' in req_data and not req_data.get('first_name'):
parts = str(req_data['name']).strip().split()
req_data['first_name'] = parts[0] if parts else ''
req_data['last_name'] = ' '.join(parts[1:]) if len(parts) > 1 else ''
serializer = UserRegisterSerializer(data=req_data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() user = serializer.save()
audit_logger.info(f"AUDIT - User Register: Username '{serializer.validated_data.get('username')}' registered successfully.") profile = getattr(user, 'customer_profile', None)
return Response({"message": "User registered successfully"}, status=status.HTTP_201_CREATED) audit_logger.info(f"AUDIT - User Register: Username/Phone '{phone_10}' registered successfully.")
else: return Response({
username = request.data.get('username') "message": "User registered successfully",
phone = request.data.get('phone', username) "username": user.username,
if username and User.objects.filter(username=username).exists(): "phone": profile.phone if profile else user.username,
user = User.objects.get(username=username) "first_name": user.first_name,
CustomerProfile.objects.get_or_create(user=user, defaults={'phone': phone or username}) "last_name": user.last_name,
return Response({"message": "User account already registered"}, status=status.HTTP_200_OK) "full_name": f"{user.first_name} {user.last_name}".strip() or user.username,
"email": user.email,
"wallet_balance": float(profile.wallet_balance) if profile else 0.0,
"loyalty_coins": profile.loyalty_coins if profile else 200,
"address": profile.default_address if profile else ""
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserProfileView(APIView): class UserProfileView(APIView):
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
def get(self, request): def get(self, request):
username = request.query_params.get('username') or request.query_params.get('phone') identifier = request.query_params.get('phone') or request.query_params.get('username')
user = None user = None
if request.user and request.user.is_authenticated: if request.user and request.user.is_authenticated:
user = request.user user = request.user
elif username: elif identifier:
clean_digits = ''.join(c for c in str(username) if c.isdigit()) clean_digits = ''.join(c for c in str(identifier) if c.isdigit())
raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits
from django.db.models import Q user = (
user = User.objects.filter(Q(username__icontains=raw_10) | Q(username=username)).first() User.objects.filter(username=raw_10).first()
or User.objects.filter(customer_profile__phone=raw_10).first()
or User.objects.filter(username=str(identifier)).first()
)
if not user: if not user:
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND) return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)
profile = getattr(user, 'customer_profile', None) profile, _ = CustomerProfile.objects.get_or_create(user=user, defaults={'phone': user.username})
return Response({ return Response({
"username": user.username, "username": user.username,
"first_name": user.first_name, "first_name": user.first_name,
"last_name": user.last_name, "last_name": user.last_name,
"full_name": f"{user.first_name} {user.last_name}".strip() or user.username,
"email": user.email, "email": user.email,
"phone": profile.phone if profile else user.username, "phone": profile.phone or user.username,
"gender": profile.gender if profile else "Prefer not to say", "gender": profile.gender or "Prefer not to say",
"wallet_balance": float(profile.wallet_balance) if profile else 0.0, "wallet_balance": float(profile.wallet_balance),
"loyalty_coins": profile.loyalty_coins if profile else 200, "loyalty_coins": profile.loyalty_coins,
"address": profile.default_address if profile else "" "address": profile.default_address or ""
}) }, status=status.HTTP_200_OK)
def put(self, request): def put(self, request):
username = request.data.get('username') or request.data.get('phone') identifier = request.data.get('phone') or request.data.get('username')
user = None user = None
if request.user and request.user.is_authenticated: if request.user and request.user.is_authenticated:
user = request.user user = request.user
elif username: elif identifier:
clean_digits = ''.join(c for c in str(username) if c.isdigit()) clean_digits = ''.join(c for c in str(identifier) if c.isdigit())
raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits raw_10 = clean_digits[-10:] if len(clean_digits) >= 10 else clean_digits
from django.db.models import Q user = (
user = User.objects.filter(Q(username__icontains=raw_10) | Q(username=username)).first() User.objects.filter(username=raw_10).first()
or User.objects.filter(customer_profile__phone=raw_10).first()
or User.objects.filter(username=str(identifier)).first()
)
if not user: if not user:
return Response({"error": "User profile not found"}, status=status.HTTP_404_NOT_FOUND) return Response({"error": "User profile not found"}, status=status.HTTP_404_NOT_FOUND)
@ -80,12 +137,21 @@ class UserProfileView(APIView):
user.first_name = request.data['first_name'] user.first_name = request.data['first_name']
if 'last_name' in request.data: if 'last_name' in request.data:
user.last_name = request.data['last_name'] user.last_name = request.data['last_name']
if 'full_name' in request.data:
parts = str(request.data['full_name']).strip().split()
user.first_name = parts[0] if parts else ''
user.last_name = ' '.join(parts[1:]) if len(parts) > 1 else ''
elif 'name' in request.data:
parts = str(request.data['name']).strip().split()
user.first_name = parts[0] if parts else ''
user.last_name = ' '.join(parts[1:]) if len(parts) > 1 else ''
user.save() user.save()
from orders.models import CustomerProfile profile, _ = CustomerProfile.objects.get_or_create(user=user, defaults={'phone': user.username})
profile, _ = CustomerProfile.objects.get_or_create(user=user) if 'phone' in request.data and request.data['phone']:
if 'phone' in request.data: clean_phone_digits = ''.join(c for c in str(request.data['phone']) if c.isdigit())
profile.phone = request.data['phone'] new_phone_10 = clean_phone_digits[-10:] if len(clean_phone_digits) >= 10 else clean_phone_digits
profile.phone = new_phone_10
if 'gender' in request.data: if 'gender' in request.data:
profile.gender = request.data['gender'] profile.gender = request.data['gender']
if 'wallet_balance' in request.data: if 'wallet_balance' in request.data:
@ -101,12 +167,15 @@ class UserProfileView(APIView):
return Response({ return Response({
"message": "Profile updated successfully", "message": "Profile updated successfully",
"username": user.username, "username": user.username,
"phone": profile.phone or user.username,
"email": user.email, "email": user.email,
"first_name": user.first_name, "first_name": user.first_name,
"last_name": user.last_name, "last_name": user.last_name,
"full_name": f"{user.first_name} {user.last_name}".strip() or user.username,
"gender": profile.gender, "gender": profile.gender,
"wallet_balance": float(profile.wallet_balance), "wallet_balance": float(profile.wallet_balance),
"loyalty_coins": profile.loyalty_coins "loyalty_coins": profile.loyalty_coins,
"address": profile.default_address
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
def send_msg91_whatsapp_direct(mobile_number, otp_code, template_name="mobile_number_verification"): def send_msg91_whatsapp_direct(mobile_number, otp_code, template_name="mobile_number_verification"):
@ -187,39 +256,37 @@ class SendWhatsAppOTPView(APIView):
return Response({"error": "mobile_number and otp_code are required"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "mobile_number and otp_code are required"}, status=status.HTTP_400_BAD_REQUEST)
clean_phone = str(mobile_number).replace('+', '').strip() clean_phone = str(mobile_number).replace('+', '').strip()
raw_10 = clean_phone[-10:] if len(clean_phone) >= 10 else clean_phone raw_digits = ''.join(c for c in clean_phone if c.isdigit())
phone_10 = raw_digits[-10:] if len(raw_digits) >= 10 else raw_digits
# Account existence validation if not phone_10 or len(phone_10) != 10:
from django.db.models import Q return Response({"error": "Please enter a valid 10-digit mobile number"}, status=status.HTTP_400_BAD_REQUEST)
user_exists = None
try:
from orders.models import CustomerProfile
user_exists = (
User.objects.filter(Q(username__icontains=raw_10) | Q(email__icontains=raw_10)).exists()
or CustomerProfile.objects.filter(Q(phone__icontains=raw_10) | Q(user__username__icontains=raw_10)).exists()
)
except Exception as db_err:
audit_logger.warning(f"DB lookup notice for {mobile_number}: {db_err}")
user_exists = None
if purpose == 'login' and user_exists is False: # Account existence validation using phone number as primary key
audit_logger.warning(f"AUDIT - Login attempt for non-existent mobile: '{mobile_number}'") user_exists = (
User.objects.filter(username=phone_10).exists()
or CustomerProfile.objects.filter(phone=phone_10).exists()
)
if purpose == 'login' and not user_exists:
audit_logger.warning(f"AUDIT - Login attempt for non-existent mobile: '{phone_10}'")
return Response({ return Response({
"status": "error", "status": "error",
"message": "Account not found. Please create a patron account first." "message": "Account not found. Please create a patron account first."
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
if purpose == 'register' and user_exists is True: if purpose == 'register' and user_exists:
audit_logger.warning(f"AUDIT - Register attempt for existing mobile: '{mobile_number}'") audit_logger.warning(f"AUDIT - Register attempt for existing mobile: '{phone_10}'")
return Response({ return Response({
"status": "error", "status": "error",
"message": "An account with this mobile number already exists. Please Sign In." "message": "An account with this phone number already exists. Please Sign In."
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
audit_logger.info(f"AUDIT - Send WhatsApp OTP: mobile '{mobile_number}', OTP '{otp_code}'") audit_logger.info(f"AUDIT - Send WhatsApp OTP: mobile '{phone_10}', OTP '{otp_code}'")
# Store OTP in backend cache for 5 minutes (300 seconds) # Store OTP in backend cache for 5 minutes (300 seconds)
cache.set(f"whatsapp_otp_{clean_phone}", str(otp_code), timeout=300) cache.set(f"whatsapp_otp_{clean_phone}", str(otp_code), timeout=300)
cache.set(f"whatsapp_otp_{phone_10}", str(otp_code), timeout=300)
whatsapp_urls = [ whatsapp_urls = [
getattr(settings, 'WHATSAPP_SERVICE_URL', 'https://betawhatsapp.tradhox.com/api/send/'), getattr(settings, 'WHATSAPP_SERVICE_URL', 'https://betawhatsapp.tradhox.com/api/send/'),
@ -313,7 +380,10 @@ class VerifyOTPView(APIView):
return Response({"error": "mobile_number and otp_code are required"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "mobile_number and otp_code are required"}, status=status.HTTP_400_BAD_REQUEST)
clean_phone = str(mobile_number).replace('+', '').strip() clean_phone = str(mobile_number).replace('+', '').strip()
stored_otp = cache.get(f"whatsapp_otp_{clean_phone}") raw_digits = ''.join(c for c in clean_phone if c.isdigit())
phone_10 = raw_digits[-10:] if len(raw_digits) >= 10 else clean_digits
stored_otp = cache.get(f"whatsapp_otp_{clean_phone}") or cache.get(f"whatsapp_otp_{phone_10}")
if stored_otp and str(stored_otp).strip() == str(otp_code).strip(): if stored_otp and str(stored_otp).strip() == str(otp_code).strip():
audit_logger.info(f"AUDIT - OTP Verification SUCCESS: mobile '{mobile_number}'") audit_logger.info(f"AUDIT - OTP Verification SUCCESS: mobile '{mobile_number}'")
@ -327,5 +397,3 @@ class VerifyOTPView(APIView):
"status": "error", "status": "error",
"message": "Invalid or expired OTP verification code. Access denied." "message": "Invalid or expired OTP verification code. Access denied."
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)