From f587c4dd24af217341d461e3d10700dd712f4d8f Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Wed, 22 Apr 2026 11:38:39 +0530 Subject: [PATCH] =?UTF-8?q?Sprint=205:=20PartnerCustomerListView=20?= =?UTF-8?q?=E2=80=94=20partner-scoped=20customer=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin_api/views.py: PartnerCustomerListView — distinct users who've booked partner's events, annotated with bookings_count + total_spent aggregates, search by email/name, paginated [1,200] - admin_api/urls.py: wire partners/me/customers/ Co-Authored-By: Claude Sonnet 4.6 --- admin_api/urls.py | 2 ++ admin_api/views.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/admin_api/urls.py b/admin_api/urls.py index 5f84a65..d94d448 100644 --- a/admin_api/urls.py +++ b/admin_api/urls.py @@ -35,6 +35,8 @@ urlpatterns = [ path('partners/me/events//tiers//', views.PartnerMeEventTierDetailView.as_view(), name='partner-me-event-tier-detail'), # Partner-Me: bookings (Sprint 4) path('partners/me/bookings/', views.PartnerBookingListView.as_view(), name='partner-me-bookings'), + # Partner-Me: customers (Sprint 5) + path('partners/me/customers/', views.PartnerCustomerListView.as_view(), name='partner-me-customers'), path('users/metrics/', views.UserMetricsView.as_view(), name='user-metrics'), path('users/', views.UserListView.as_view(), name='user-list'), path('users//', views.UserDetailView.as_view(), name='user-detail'), diff --git a/admin_api/views.py b/admin_api/views.py index fa4765a..24e06c8 100644 --- a/admin_api/views.py +++ b/admin_api/views.py @@ -3985,3 +3985,88 @@ class PartnerBookingListView(APIView): 'pageSize': page_size, 'results': results, }) + + +# ============================================================ +# Sprint 5 — Partner Customers (Users who booked partner events) +# ============================================================ + +class PartnerCustomerListView(APIView): + """ + GET /api/v1/partners/me/customers/ + Returns distinct users who have made bookings for this partner's events. + Query params: search, page, page_size + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + from bookings.models import Booking + from accounts.models import User + from django.db.models import Count, Sum, F, Q, DecimalField, ExpressionWrapper + + partner, err = _require_partner(request) + if err: + return err + + # Annotate users with per-partner booking stats + user_qs = User.objects.filter( + booking__ticket_meta__event__partner=partner + ).annotate( + bookings_count=Count( + 'booking', + filter=Q(booking__ticket_meta__event__partner=partner), + ), + total_spent=Sum( + ExpressionWrapper( + F('booking__price') * F('booking__quantity'), + output_field=DecimalField(max_digits=12, decimal_places=2), + ), + filter=Q(booking__ticket_meta__event__partner=partner), + ), + ).distinct().order_by('-bookings_count', 'id') + + # Search by email / name + search = request.query_params.get('search', '').strip() + if search: + user_qs = user_qs.filter( + Q(email__icontains=search) | + Q(first_name__icontains=search) | + Q(last_name__icontains=search) | + Q(username__icontains=search) + ) + + # Pagination + try: + page_size = max(1, min(int(request.query_params.get('page_size', 20)), 200)) + except (ValueError, TypeError): + page_size = 20 + try: + page = max(1, int(request.query_params.get('page', 1))) + except (ValueError, TypeError): + page = 1 + + total = user_qs.count() + start = (page - 1) * page_size + users = user_qs[start:start + page_size] + + results = [] + for u in users: + display_name = f"{u.first_name} {u.last_name}".strip() or u.username + results.append({ + 'id': str(u.id), + 'name': display_name, + 'email': u.email, + 'phone': u.phone if hasattr(u, 'phone') else '', + 'bookingsCount': u.bookings_count or 0, + 'totalSpent': str(u.total_spent or 0), + 'joinedAt': u.date_joined.isoformat() if u.date_joined else None, + 'lastLogin': u.last_login.isoformat() if u.last_login else None, + 'isActive': u.is_active, + }) + + return Response({ + 'count': total, + 'page': page, + 'pageSize': page_size, + 'results': results, + })