@@ -4223,3 +4223,194 @@ 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 " : " <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 ,
} )
# ============================================================
# Sprint 8 — Partner Dashboard Aggregate
# ============================================================
class PartnerDashboardView ( APIView ) :
"""
GET /api/v1/partners/me/dashboard/
Returns partner KPIs, recent bookings, and upcoming events in one call.
"""
permission_classes = [ IsAuthenticated ]
def get ( self , request ) :
from bookings . models import Booking
from django . db . models import Sum , Count , F , Q , DecimalField , ExpressionWrapper
from datetime import date , timedelta
partner , err = _require_partner ( request )
if err :
return err
today = date . today ( )
# Current period: last 30 days
period_start = today - timedelta ( days = 30 )
# Previous period: 31– 60 days ago (for change %)
prev_start = today - timedelta ( days = 60 )
prev_end = today - timedelta ( days = 31 )
def booking_revenue_qs ( start , end ) :
return Booking . objects . filter (
ticket_meta__event__partner = partner ,
payment_status = ' paid ' ,
created_date__gte = start ,
created_date__lte = end ,
) . aggregate (
total = Sum (
ExpressionWrapper (
F ( ' price ' ) * F ( ' quantity ' ) ,
output_field = DecimalField ( max_digits = 14 , decimal_places = 2 ) ,
)
) ,
tickets = Sum ( ' quantity ' ) ,
)
curr = booking_revenue_qs ( period_start , today )
prev = booking_revenue_qs ( prev_start , prev_end )
curr_rev = float ( curr [ ' total ' ] or 0 )
prev_rev = float ( prev [ ' total ' ] or 0 )
curr_tickets = int ( curr [ ' tickets ' ] or 0 )
prev_tickets = int ( prev [ ' tickets ' ] or 0 )
def pct_change ( curr_v , prev_v ) :
if prev_v == 0 :
return ' +0 % ' if curr_v == 0 else ' +100 % '
delta = ( ( curr_v - prev_v ) / prev_v ) * 100
sign = ' + ' if delta > = 0 else ' '
return f ' { sign } { delta : .1f } % '
from events . models import Event
active_events = Event . objects . filter (
partner = partner , event_status__in = [ ' published ' , ' live ' ]
) . count ( )
total_events = Event . objects . filter ( partner = partner ) . count ( )
# KPIs
kpis = {
' totalRevenue ' : f ' \u20b9 { curr_rev : ,.0f } ' ,
' revenueChange ' : pct_change ( curr_rev , prev_rev ) ,
' ticketsSold ' : f ' { curr_tickets : , } ' ,
' ticketsChange ' : pct_change ( curr_tickets , prev_tickets ) ,
' activeEvents ' : str ( active_events ) ,
' totalEvents ' : str ( total_events ) ,
}
# Recent bookings (last 5 paid + pending, newest first)
recent_qs = Booking . objects . filter (
ticket_meta__event__partner = partner ,
) . select_related ( ' user ' , ' ticket_meta__event ' , ' ticket_type ' ) . order_by (
' -created_date ' , ' -id '
) [ : 5 ]
recent_bookings = [ ]
for b in recent_qs :
user = b . user
name = f " { user . first_name } { user . last_name } " . strip ( ) or user . username
event = b . ticket_meta . event if b . ticket_meta_id else None
recent_bookings . append ( {
' id ' : b . booking_id or f ' BKG- { b . id } ' ,
' event ' : event . title if event else ' — ' ,
' customer ' : name ,
' date ' : b . created_date . isoformat ( ) if b . created_date else None ,
' amount ' : f ' \u20b9 { float ( b . price * b . quantity ) : ,.0f } ' ,
' status ' : b . payment_status ,
} )
# Upcoming events (next 3 by date, have a future start date or are published)
upcoming_qs = Event . objects . filter (
partner = partner ,
event_status__in = [ ' published ' , ' live ' , ' draft ' ] ,
) . order_by ( ' event_date ' , ' id ' ) [ : 5 ]
upcoming_events = [ ]
for ev in upcoming_qs :
# Count tickets sold for this event
sold = Booking . objects . filter (
ticket_meta__event = ev , payment_status = ' paid '
) . aggregate ( s = Sum ( ' quantity ' ) ) [ ' s ' ] or 0
# Get total capacity from TicketMeta
from bookings . models import TicketMeta
try :
meta = TicketMeta . objects . get ( event = ev )
total_capacity = meta . maximum_quantity or 0
except TicketMeta . DoesNotExist :
total_capacity = 0
upcoming_events . append ( {
' id ' : str ( ev . id ) ,
' name ' : ev . title ,
' date ' : ev . event_date . isoformat ( ) if ev . event_date else None ,
' sold ' : sold ,
' total ' : total_capacity ,
' status ' : ev . event_status ,
} )
return Response ( {
' kpis ' : kpis ,
' recentBookings ' : recent_bookings ,
' upcomingEvents ' : upcoming_events ,
} )