diff --git a/admin_api/urls.py b/admin_api/urls.py index 68840cd..5f84a65 100644 --- a/admin_api/urls.py +++ b/admin_api/urls.py @@ -33,6 +33,8 @@ urlpatterns = [ # Partner-Me: ticket tiers (Sprint 3) path('partners/me/events//tiers/', views.PartnerMeEventTiersView.as_view(), name='partner-me-event-tiers'), 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'), 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 b6adc93..fa4765a 100644 --- a/admin_api/views.py +++ b/admin_api/views.py @@ -3897,3 +3897,91 @@ class PartnerMeEventTierDetailView(APIView): return err tt.delete() return Response({'status': 'deleted'}, status=204) + + +# ============================================================ +# Sprint 4 — Partner Bookings +# ============================================================ + +class PartnerBookingListView(APIView): + """ + GET /api/v1/partners/me/bookings/ + Query params: search, status (payment_status), event_id, page, page_size + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + from bookings.models import Booking + from django.db.models import Q + + partner, err = _require_partner(request) + if err: + return err + + qs = Booking.objects.filter( + ticket_meta__event__partner=partner + ).select_related( + 'user', 'ticket_meta__event', 'ticket_type' + ).order_by('-created_date', '-id') + + # Search: booking_id, user email, user first/last name + search = request.query_params.get('search', '').strip() + if search: + qs = qs.filter( + Q(booking_id__icontains=search) | + Q(user__email__icontains=search) | + Q(user__first_name__icontains=search) | + Q(user__last_name__icontains=search) + ) + + # Filter by payment_status + status = request.query_params.get('status', '').strip() + if status: + qs = qs.filter(payment_status=status) + + # Filter by event_id + event_id = request.query_params.get('event_id', '').strip() + if event_id: + qs = qs.filter(ticket_meta__event_id=event_id) + + # 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 = qs.count() + start = (page - 1) * page_size + bookings = qs[start:start + page_size] + + results = [] + for b in bookings: + user = b.user + customer_name = f"{user.first_name} {user.last_name}".strip() or user.username + event = b.ticket_meta.event if b.ticket_meta_id else None + results.append({ + 'id': str(b.id), + 'bookingId': b.booking_id or f'BKG-{b.id}', + 'customerName': customer_name, + 'customerEmail': user.email, + 'eventTitle': event.title if event else '—', + 'eventId': str(event.id) if event else None, + 'ticketType': b.ticket_type.ticket_type if b.ticket_type_id else '—', + 'quantity': b.quantity, + 'amount': str(b.price), + 'totalAmount': str(b.price * b.quantity), + 'paymentStatus': b.payment_status, + 'transactionId': b.transaction_id or '', + 'createdDate': b.created_date.isoformat() if b.created_date else None, + }) + + return Response({ + 'count': total, + 'page': page, + 'pageSize': page_size, + 'results': results, + })