From b6c2b93fd03ae382cb7d19710212960c5be5a850 Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Wed, 22 Apr 2026 11:45:32 +0530 Subject: [PATCH] =?UTF-8?q?Sprint=207:=20PartnerMeCheckInView=20=E2=80=94?= =?UTF-8?q?=20JWT-authenticated=20ticket=20check-in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin_api/views.py: PartnerMeCheckInView — validates ticket belongs to partner's event, marks is_checked_in=True, returns name/ticket/event/ alreadyCheckedIn; uses IsAuthenticated (Bearer JWT, not body token) - admin_api/urls.py: wire partners/me/check-in/ Co-Authored-By: Claude Sonnet 4.6 --- admin_api/urls.py | 2 ++ admin_api/views.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/admin_api/urls.py b/admin_api/urls.py index 35751fc..f6d4e4a 100644 --- a/admin_api/urls.py +++ b/admin_api/urls.py @@ -40,6 +40,8 @@ urlpatterns = [ # Partner-Me: staff CRUD (Sprint 6) path('partners/me/staff/', views.PartnerMeStaffListView.as_view(), name='partner-me-staff-list'), path('partners/me/staff//', views.PartnerMeStaffDetailView.as_view(), name='partner-me-staff-detail'), + # Partner-Me: check-in (Sprint 7) + path('partners/me/check-in/', views.PartnerMeCheckInView.as_view(), name='partner-me-check-in'), 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 bf9a495..30195c1 100644 --- a/admin_api/views.py +++ b/admin_api/views.py @@ -4223,3 +4223,62 @@ class PartnerMeStaffDetailView(APIView): staff_user.is_active = False staff_user.save(update_fields=['is_active']) return Response({'status': 'revoked'}, status=204) + + +# ============================================================ +# Sprint 7 — Partner Check-in (JWT-authenticated) +# ============================================================ + +class PartnerMeCheckInView(APIView): + """ + POST /api/v1/partners/me/check-in/ + Body: { "ticket_id": "" } + Validates the ticket belongs to a partner-owned event, marks checked-in. + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + from bookings.models import Ticket + + partner, err = _require_partner(request) + if err: + return err + + ticket_id = (request.data.get('ticket_id') or '').strip() + if not ticket_id: + return Response({'valid': False, 'error': 'ticket_id is required'}, status=400) + + try: + ticket = Ticket.objects.select_related( + 'booking__user', + 'booking__ticket_meta__event', + 'booking__ticket_type', + ).get(ticket_id=ticket_id) + except Ticket.DoesNotExist: + return Response({'valid': False, 'error': 'Ticket not found'}, status=404) + + # Verify the ticket's event belongs to this partner + event = ticket.booking.ticket_meta.event if ticket.booking.ticket_meta_id else None + if not event or event.partner_id != partner.id: + return Response({'valid': False, 'error': 'Ticket not found'}, status=404) + + user = ticket.booking.user + customer_name = f"{user.first_name} {user.last_name}".strip() or user.username + ticket_type = ticket.booking.ticket_type.ticket_type if ticket.booking.ticket_type_id else '—' + + already_checked_in = ticket.is_checked_in + + if not already_checked_in: + from datetime import datetime, timezone as _tz + ticket.is_checked_in = True + ticket.checked_in_date_time = datetime.now(_tz.utc) + ticket.save(update_fields=['is_checked_in', 'checked_in_date_time']) + + return Response({ + 'valid': True, + 'alreadyCheckedIn': already_checked_in, + 'name': customer_name, + 'ticket': ticket_type, + 'event': event.title if event else '—', + 'ticketId': ticket.ticket_id, + })