Compare commits
2 Commits
sprint/3-t
...
sprint/5-c
| Author | SHA1 | Date | |
|---|---|---|---|
| f587c4dd24 | |||
| 4669907a02 |
@@ -33,6 +33,10 @@ urlpatterns = [
|
|||||||
# Partner-Me: ticket tiers (Sprint 3)
|
# Partner-Me: ticket tiers (Sprint 3)
|
||||||
path('partners/me/events/<int:event_pk>/tiers/', views.PartnerMeEventTiersView.as_view(), name='partner-me-event-tiers'),
|
path('partners/me/events/<int:event_pk>/tiers/', views.PartnerMeEventTiersView.as_view(), name='partner-me-event-tiers'),
|
||||||
path('partners/me/events/<int:event_pk>/tiers/<int:tier_pk>/', views.PartnerMeEventTierDetailView.as_view(), name='partner-me-event-tier-detail'),
|
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/metrics/', views.UserMetricsView.as_view(), name='user-metrics'),
|
||||||
path('users/', views.UserListView.as_view(), name='user-list'),
|
path('users/', views.UserListView.as_view(), name='user-list'),
|
||||||
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
||||||
|
|||||||
@@ -3897,3 +3897,176 @@ class PartnerMeEventTierDetailView(APIView):
|
|||||||
return err
|
return err
|
||||||
tt.delete()
|
tt.delete()
|
||||||
return Response({'status': 'deleted'}, status=204)
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 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