feat(leads): add Lead Manager module with full admin and consumer endpoints
- Lead model in admin_api with status/priority/source/assigned_to fields - Admin API: metrics, list, detail, update views at /api/v1/leads/ - Consumer API: public ScheduleCallView at /api/leads/schedule-call/ - RBAC: 'leads' module registered in ALL_MODULES and StaffProfile scopes - Migration 0003_lead with indexes on status, priority, created_at, email
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import *
|
||||
from mobile_api.views.user import ScheduleCallView
|
||||
from mobile_api.views.reviews import ReviewSubmitView, MobileReviewListView, ReviewHelpfulView, ReviewFlagView
|
||||
from ad_control.views import ConsumerFeaturedEventsView, ConsumerTopEventsView
|
||||
|
||||
@@ -13,6 +14,7 @@ urlpatterns = [
|
||||
path('user/update-profile/', UpdateProfileView.as_view(), name='update_profile'),
|
||||
path('user/bulk-public-info/', BulkUserPublicInfoView.as_view(), name='bulk_public_info'),
|
||||
path('user/google-login/', GoogleLoginView.as_view(), name='google_login'),
|
||||
path('leads/schedule-call/', ScheduleCallView.as_view(), name='schedule_call'),
|
||||
]
|
||||
|
||||
# Event URLS
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# accounts/views.py
|
||||
import json
|
||||
import secrets
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.authtoken.models import Token
|
||||
from mobile_api.forms import RegisterForm, LoginForm, WebRegisterForm
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
@@ -363,3 +365,150 @@ class UpdateProfileView(View):
|
||||
'success': False,
|
||||
'error': 'An unexpected server error occurred. Please try again.'
|
||||
}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class BulkUserPublicInfoView(APIView):
|
||||
"""Internal endpoint for Node.js gamification server to resolve user details.
|
||||
Accepts POST with { emails: [...] } (max 500).
|
||||
Returns { users: { email: { district, display_name, eventify_id } } }
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
json_data = json.loads(request.body)
|
||||
emails = json_data.get('emails', [])
|
||||
if not emails or not isinstance(emails, list) or len(emails) > 500:
|
||||
return JsonResponse({'error': 'Provide 1-500 emails'}, status=400)
|
||||
|
||||
users_qs = User.objects.filter(email__in=emails).values_list(
|
||||
'email', 'first_name', 'last_name', 'district', 'eventify_id'
|
||||
)
|
||||
result = {}
|
||||
for email, first, last, district, eid in users_qs:
|
||||
name = f"{first} {last}".strip() or email.split('@')[0]
|
||||
result[email] = {
|
||||
'display_name': name,
|
||||
'district': district or '',
|
||||
'eventify_id': eid or '',
|
||||
}
|
||||
return JsonResponse({'users': result})
|
||||
except Exception as e:
|
||||
log("error", "BulkUserPublicInfoView error", logger_data={"error": str(e)})
|
||||
return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class GoogleLoginView(View):
|
||||
"""Verify a Google ID token, find or create the user, return the same response shape as LoginView."""
|
||||
def post(self, request):
|
||||
try:
|
||||
from google.oauth2 import id_token as google_id_token
|
||||
from google.auth.transport import requests as google_requests
|
||||
|
||||
data = json.loads(request.body)
|
||||
token = data.get('id_token')
|
||||
if not token:
|
||||
return JsonResponse({'error': 'id_token is required'}, status=400)
|
||||
|
||||
idinfo = google_id_token.verify_oauth2_token(token, google_requests.Request())
|
||||
email = idinfo.get('email')
|
||||
if not email:
|
||||
return JsonResponse({'error': 'Email not found in Google token'}, status=400)
|
||||
|
||||
user, created = User.objects.get_or_create(
|
||||
email=email,
|
||||
defaults={
|
||||
'username': email,
|
||||
'first_name': idinfo.get('given_name', ''),
|
||||
'last_name': idinfo.get('family_name', ''),
|
||||
'role': 'customer',
|
||||
},
|
||||
)
|
||||
if created:
|
||||
user.set_password(secrets.token_urlsafe(32))
|
||||
user.save()
|
||||
log("info", "Google OAuth new user created", request=request, user=user)
|
||||
|
||||
auth_token, _ = Token.objects.get_or_create(user=user)
|
||||
log("info", "Google OAuth login", request=request, user=user)
|
||||
|
||||
return JsonResponse({
|
||||
'message': 'Login successful',
|
||||
'token': auth_token.key,
|
||||
'eventify_id': user.eventify_id or '',
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'phone_number': user.phone_number or '',
|
||||
'first_name': user.first_name,
|
||||
'last_name': user.last_name,
|
||||
'role': user.role,
|
||||
'pincode': user.pincode or '',
|
||||
'district': user.district or '',
|
||||
'district_changed_at': user.district_changed_at.isoformat() if user.district_changed_at else None,
|
||||
'state': user.state or '',
|
||||
'country': user.country or '',
|
||||
'place': user.place or '',
|
||||
'latitude': user.latitude or '',
|
||||
'longitude': user.longitude or '',
|
||||
'profile_photo': user.profile_picture.url if user.profile_picture else '',
|
||||
}, status=200)
|
||||
except ValueError as e:
|
||||
log("warning", "Google OAuth invalid token", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({'error': 'Invalid Google token'}, status=401)
|
||||
except Exception as e:
|
||||
log("error", "Google OAuth exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class ScheduleCallView(View):
|
||||
"""Public endpoint for the 'Schedule a Call' form on the consumer app."""
|
||||
|
||||
def post(self, request):
|
||||
from admin_api.models import Lead
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
name = (data.get('name') or '').strip()
|
||||
email = (data.get('email') or '').strip()
|
||||
phone = (data.get('phone') or '').strip()
|
||||
event_type = (data.get('eventType') or '').strip()
|
||||
message = (data.get('message') or '').strip()
|
||||
|
||||
errors = {}
|
||||
if not name:
|
||||
errors['name'] = ['This field is required.']
|
||||
if not email:
|
||||
errors['email'] = ['This field is required.']
|
||||
if not phone:
|
||||
errors['phone'] = ['This field is required.']
|
||||
valid_event_types = [c[0] for c in Lead.EVENT_TYPE_CHOICES]
|
||||
if not event_type or event_type not in valid_event_types:
|
||||
errors['eventType'] = [f'Must be one of: {", ".join(valid_event_types)}']
|
||||
|
||||
if errors:
|
||||
return JsonResponse({'errors': errors}, status=400)
|
||||
|
||||
lead = Lead.objects.create(
|
||||
name=name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
event_type=event_type,
|
||||
message=message,
|
||||
status='new',
|
||||
source='schedule_call',
|
||||
priority='medium',
|
||||
)
|
||||
log("info", f"New schedule-call lead #{lead.pk} from {email}", request=request)
|
||||
return JsonResponse({
|
||||
'status': 'success',
|
||||
'message': 'Your request has been submitted. Our team will get back to you soon.',
|
||||
'lead_id': lead.pk,
|
||||
}, status=201)
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({'error': 'Invalid JSON body.'}, status=400)
|
||||
except Exception as e:
|
||||
log("error", "Schedule call exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({'error': 'An unexpected server error occurred.'}, status=500)
|
||||
|
||||
Reference in New Issue
Block a user