Sprint 5: PartnerCustomerListView — partner-scoped customer list
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,8 @@ urlpatterns = [
|
||||
path('partners/me/events/<int:event_pk>/tiers/<int:tier_pk>/', 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/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user