customerwebsitebackend/api/views.py
vickytechkey 4ca070ad8c
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 35s
feat: implement customer registration, profile management, and auth tests
2026-11-01 20:53:36 +05:30

399 lines
17 KiB
Python

import logging
import json
import urllib.request
import urllib.error
from django.conf import settings
from django.core.cache import cache
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from django.contrib.auth.models import User
from orders.models import CustomerProfile
from .serializers import UserRegisterSerializer
audit_logger = logging.getLogger('audit')
class UserRegisterView(APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
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():
user = serializer.save()
profile = getattr(user, 'customer_profile', None)
audit_logger.info(f"AUDIT - User Register: Username/Phone '{phone_10}' registered successfully.")
return Response({
"message": "User registered successfully",
"username": user.username,
"phone": profile.phone if profile else user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"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)
class UserProfileView(APIView):
permission_classes = [permissions.AllowAny]
def get(self, request):
identifier = request.query_params.get('phone') or request.query_params.get('username')
user = None
if request.user and request.user.is_authenticated:
user = request.user
elif identifier:
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
user = (
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:
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)
profile, _ = CustomerProfile.objects.get_or_create(user=user, defaults={'phone': user.username})
return Response({
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"full_name": f"{user.first_name} {user.last_name}".strip() or user.username,
"email": user.email,
"phone": profile.phone or user.username,
"gender": profile.gender or "Prefer not to say",
"wallet_balance": float(profile.wallet_balance),
"loyalty_coins": profile.loyalty_coins,
"address": profile.default_address or ""
}, status=status.HTTP_200_OK)
def put(self, request):
identifier = request.data.get('phone') or request.data.get('username')
user = None
if request.user and request.user.is_authenticated:
user = request.user
elif identifier:
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
user = (
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:
return Response({"error": "User profile not found"}, status=status.HTTP_404_NOT_FOUND)
if 'email' in request.data:
user.email = request.data['email']
if 'first_name' in request.data:
user.first_name = request.data['first_name']
if 'last_name' in request.data:
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()
profile, _ = CustomerProfile.objects.get_or_create(user=user, defaults={'phone': user.username})
if 'phone' in request.data and request.data['phone']:
clean_phone_digits = ''.join(c for c in str(request.data['phone']) if c.isdigit())
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:
profile.gender = request.data['gender']
if 'wallet_balance' in request.data:
profile.wallet_balance = request.data['wallet_balance']
if 'loyalty_coins' in request.data:
profile.loyalty_coins = request.data['loyalty_coins']
if 'address' in request.data:
profile.default_address = request.data['address']
profile.save()
audit_logger.info(f"AUDIT - User Profile Updated: Username '{user.username}', Email '{user.email}', Gender '{profile.gender}'")
return Response({
"message": "Profile updated successfully",
"username": user.username,
"phone": profile.phone or user.username,
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"full_name": f"{user.first_name} {user.last_name}".strip() or user.username,
"gender": profile.gender,
"wallet_balance": float(profile.wallet_balance),
"loyalty_coins": profile.loyalty_coins,
"address": profile.default_address
}, status=status.HTTP_200_OK)
def send_msg91_whatsapp_direct(mobile_number, otp_code, template_name="mobile_number_verification"):
clean_digits = ''.join(c for c in str(mobile_number) if c.isdigit())
target_number = f"91{clean_digits}" if len(clean_digits) == 10 else clean_digits
msg91_payload = {
"integrated_number": "918610694498",
"content_type": "template",
"payload": {
"messaging_product": "whatsapp",
"type": "template",
"template": {
"name": template_name,
"language": {
"code": "en",
"policy": "deterministic"
},
"namespace": None,
"to_and_components": [
{
"to": [target_number],
"components": {
"body_1": {
"type": "text",
"value": str(otp_code)
},
"button_1": {
"subtype": "url",
"type": "text",
"value": str(otp_code)
}
}
}
]
}
}
}
try:
req_data = json.dumps(msg91_payload).encode('utf-8')
req = urllib.request.Request(
'https://control.msg91.com/api/v5/whatsapp/whatsapp-outbound-message/bulk/',
data=req_data,
headers={
'accept': 'application/json',
'authkey': '563180AluxJHIeTGn6aaf97aeP1',
'content-type': 'application/json'
}
)
with urllib.request.urlopen(req, timeout=10) as resp:
body_bytes = resp.read()
try:
res_data = json.loads(body_bytes.decode('utf-8'))
except Exception:
res_data = {"text": body_bytes.decode('utf-8', errors='ignore')}
return resp.status, res_data
except urllib.error.HTTPError as e:
err_body = e.read().decode('utf-8', errors='ignore')
try:
err_data = json.loads(err_body)
except Exception:
err_data = {"text": err_body}
return e.code, err_data
except Exception as e:
return 500, {"error": str(e)}
class SendWhatsAppOTPView(APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
mobile_number = request.data.get('mobile_number')
otp_code = request.data.get('otp_code')
template_name = request.data.get('template_name', 'mobile_number_verification')
purpose = request.data.get('purpose', 'login')
if not mobile_number or not otp_code:
return Response({"error": "mobile_number and otp_code are required"}, status=status.HTTP_400_BAD_REQUEST)
clean_phone = str(mobile_number).replace('+', '').strip()
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
if not phone_10 or len(phone_10) != 10:
return Response({"error": "Please enter a valid 10-digit mobile number"}, status=status.HTTP_400_BAD_REQUEST)
# Account existence validation using phone number as primary key
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({
"status": "error",
"message": "Account not found. Please create a patron account first."
}, status=status.HTTP_404_NOT_FOUND)
if purpose == 'register' and user_exists:
audit_logger.warning(f"AUDIT - Register attempt for existing mobile: '{phone_10}'")
return Response({
"status": "error",
"message": "An account with this phone number already exists. Please Sign In."
}, status=status.HTTP_400_BAD_REQUEST)
audit_logger.info(f"AUDIT - Send WhatsApp OTP: mobile '{phone_10}', OTP '{otp_code}'")
# 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_{phone_10}", str(otp_code), timeout=300)
whatsapp_urls = [
getattr(settings, 'WHATSAPP_SERVICE_URL', 'https://betawhatsapp.tradhox.com/api/send/'),
'https://betawhatsapp.tradhox.com/send/',
'http://betawhatsapp.tradhox.com/api/send/',
'http://betawhatsapp.tradhox.com/send/',
'http://127.0.0.1:8081/api/send/',
'http://127.0.0.1:8081/send/'
]
payload = {
"mobile_number": mobile_number,
"otp_code": otp_code,
"template_name": template_name
}
req_data = json.dumps(payload).encode('utf-8')
last_error = None
res_data = None
status_code = 500
for url in whatsapp_urls:
try:
req = urllib.request.Request(
url,
data=req_data,
headers={'Content-Type': 'application/json', 'User-Agent': 'TradhoxBackend/1.0'}
)
with urllib.request.urlopen(req, timeout=6) as resp:
status_code = resp.status
body_bytes = resp.read()
try:
res_data = json.loads(body_bytes.decode('utf-8'))
except Exception:
res_data = {"text": body_bytes.decode('utf-8', errors='ignore')}
if status_code == 200 and res_data.get('status') != 'error':
return Response({
"status": "success",
"message": f"WhatsApp OTP dispatched to {mobile_number}",
"mobile_number": mobile_number,
"gateway_response": res_data
}, status=status.HTTP_200_OK)
except urllib.error.HTTPError as e:
status_code = e.code
err_body = e.read().decode('utf-8', errors='ignore')
try:
res_data = json.loads(err_body)
except Exception:
res_data = {"text": err_body}
last_error = f"HTTP {e.code}: {res_data}"
audit_logger.warning(f"WhatsApp gateway attempt {url} returned HTTP {e.code}: {res_data}")
except Exception as e:
last_error = str(e)
audit_logger.warning(f"WhatsApp gateway attempt {url} failed: {e}")
# Fallback directly to MSG91 engine
audit_logger.info(f"Attempting direct MSG91 WhatsApp dispatch for {mobile_number}")
msg91_status, msg91_res = send_msg91_whatsapp_direct(mobile_number, otp_code, template_name)
if msg91_status == 200 and msg91_res.get('status') == 'success':
return Response({
"status": "success",
"message": f"WhatsApp OTP dispatched to {mobile_number}",
"mobile_number": mobile_number,
"gateway_response": msg91_res
}, status=status.HTTP_200_OK)
if status_code == 200:
return Response({
"status": "success",
"message": f"WhatsApp OTP request submitted for {mobile_number}",
"mobile_number": mobile_number,
"gateway_response": res_data
}, status=status.HTTP_200_OK)
else:
return Response({
"status": "error",
"message": f"WhatsApp service gateway error: {last_error or msg91_res}",
"mobile_number": mobile_number
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class VerifyOTPView(APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
mobile_number = request.data.get('mobile_number')
otp_code = request.data.get('otp_code')
if not mobile_number or not otp_code:
return Response({"error": "mobile_number and otp_code are required"}, status=status.HTTP_400_BAD_REQUEST)
clean_phone = str(mobile_number).replace('+', '').strip()
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():
audit_logger.info(f"AUDIT - OTP Verification SUCCESS: mobile '{mobile_number}'")
return Response({
"status": "success",
"message": "OTP verified successfully"
}, status=status.HTTP_200_OK)
else:
audit_logger.warning(f"AUDIT - OTP Verification FAILED: mobile '{mobile_number}', submitted '{otp_code}', expected '{stored_otp}'")
return Response({
"status": "error",
"message": "Invalid or expired OTP verification code. Access denied."
}, status=status.HTTP_400_BAD_REQUEST)