- GET /api/v1/partners/stats/ - total, active, pendingKyc, highRisk counts - GET /api/v1/partners/ - paginated list with status/kyc/type/search filters - GET /api/v1/partners/:id/ - full detail with events, kycDocuments, dealTerms, ledger - PATCH /api/v1/partners/:id/status/ - suspend/activate partner - POST /api/v1/partners/:id/kyc/review/ - approve/reject KYC with reason Helpers: _serialize_partner(), _partner_kyc_docs() Status/KYC/type mapping: backend snake_case to frontend capitalised values Risk score derived from kyc_compliance_status (high_risk=80, approved=5, etc.) All views IsAuthenticated, models imported inside methods
474 lines
18 KiB
Python
474 lines
18 KiB
Python
from django.contrib.auth import authenticate, get_user_model
|
|
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework import status
|
|
from rest_framework_simplejwt.tokens import RefreshToken
|
|
from rest_framework_simplejwt.views import TokenRefreshView
|
|
from django.db import connection
|
|
from .serializers import UserSerializer
|
|
|
|
User = get_user_model()
|
|
|
|
class AdminLoginView(APIView):
|
|
permission_classes = [AllowAny]
|
|
def post(self, request):
|
|
identifier = request.data.get('username') or request.data.get('email')
|
|
password = request.data.get('password')
|
|
if not identifier or not password:
|
|
return Response({'error': 'username/email and password required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
# Try username first, then email
|
|
user = authenticate(request, username=identifier, password=password)
|
|
if not user:
|
|
try:
|
|
u = User.objects.get(email=identifier)
|
|
user = authenticate(request, username=u.username, password=password)
|
|
except User.DoesNotExist:
|
|
pass
|
|
if not user:
|
|
return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED)
|
|
if not user.is_active:
|
|
return Response({'error': 'Account is disabled'}, status=status.HTTP_403_FORBIDDEN)
|
|
refresh = RefreshToken.for_user(user)
|
|
return Response({
|
|
'access': str(refresh.access_token),
|
|
'refresh': str(refresh),
|
|
'user': UserSerializer(user).data,
|
|
})
|
|
|
|
class MeView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
def get(self, request):
|
|
return Response({'user': UserSerializer(request.user).data})
|
|
|
|
class HealthView(APIView):
|
|
permission_classes = [AllowAny]
|
|
def get(self, request):
|
|
try:
|
|
connection.ensure_connection()
|
|
db_status = 'ok'
|
|
except Exception:
|
|
db_status = 'error'
|
|
return Response({'status': 'ok', 'db': db_status})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 2: Dashboard Views
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class DashboardMetricsView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
from ledger.models import RazorpayTransaction
|
|
from partner.models import Partner
|
|
from events.models import Event
|
|
from bookings.models import Ticket, Booking
|
|
from django.db.models import Sum
|
|
from django.utils import timezone
|
|
import datetime
|
|
|
|
today = timezone.now().date()
|
|
|
|
# --- Revenue ---
|
|
total_paise = (
|
|
RazorpayTransaction.objects
|
|
.filter(status='captured')
|
|
.aggregate(total=Sum('amount'))['total'] or 0
|
|
)
|
|
total_revenue = total_paise / 100
|
|
|
|
# This-month / last-month revenue for growth
|
|
first_of_this_month = today.replace(day=1)
|
|
first_of_last_month = (first_of_this_month - datetime.timedelta(days=1)).replace(day=1)
|
|
|
|
this_month_paise = (
|
|
RazorpayTransaction.objects
|
|
.filter(status='captured', captured_at__date__gte=first_of_this_month)
|
|
.aggregate(total=Sum('amount'))['total'] or 0
|
|
)
|
|
last_month_paise = (
|
|
RazorpayTransaction.objects
|
|
.filter(
|
|
status='captured',
|
|
captured_at__date__gte=first_of_last_month,
|
|
captured_at__date__lt=first_of_this_month,
|
|
)
|
|
.aggregate(total=Sum('amount'))['total'] or 0
|
|
)
|
|
this_month_rev = this_month_paise / 100
|
|
last_month_rev = last_month_paise / 100
|
|
revenue_growth = (
|
|
round((this_month_rev - last_month_rev) / last_month_rev * 100, 2)
|
|
if last_month_rev else 0
|
|
)
|
|
|
|
# --- Partners ---
|
|
active_partners = Partner.objects.filter(status='active').count()
|
|
pending_partners = Partner.objects.filter(kyc_compliance_status='pending').count()
|
|
|
|
# --- Events ---
|
|
live_events = Event.objects.filter(event_status='live').count()
|
|
events_today = Event.objects.filter(start_date=today).count()
|
|
|
|
# --- Tickets ---
|
|
ticket_sales = Ticket.objects.count()
|
|
|
|
# This-week / last-week ticket growth
|
|
week_start = today - datetime.timedelta(days=today.weekday()) # Monday
|
|
last_week_start = week_start - datetime.timedelta(days=7)
|
|
|
|
this_week_tickets = Ticket.objects.filter(
|
|
booking__created_date__gte=week_start
|
|
).count()
|
|
last_week_tickets = Ticket.objects.filter(
|
|
booking__created_date__gte=last_week_start,
|
|
booking__created_date__lt=week_start,
|
|
).count()
|
|
ticket_growth = (
|
|
round((this_week_tickets - last_week_tickets) / last_week_tickets * 100, 2)
|
|
if last_week_tickets else 0
|
|
)
|
|
|
|
return Response({
|
|
'totalRevenue': total_revenue,
|
|
'revenueGrowth': revenue_growth,
|
|
'activePartners': active_partners,
|
|
'pendingPartners': pending_partners,
|
|
'liveEvents': live_events,
|
|
'eventsToday': events_today,
|
|
'ticketSales': ticket_sales,
|
|
'ticketGrowth': ticket_growth,
|
|
})
|
|
|
|
|
|
class DashboardRevenueView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
from ledger.models import RazorpayTransaction
|
|
from banking_operations.models import PaymentTransaction
|
|
from django.db.models import Sum
|
|
from django.utils import timezone
|
|
import datetime
|
|
|
|
today = timezone.now().date()
|
|
result = []
|
|
|
|
for i in range(6, -1, -1):
|
|
day = today - datetime.timedelta(days=i)
|
|
|
|
rev_paise = (
|
|
RazorpayTransaction.objects
|
|
.filter(status='captured', captured_at__date=day)
|
|
.aggregate(total=Sum('amount'))['total'] or 0
|
|
)
|
|
revenue = rev_paise / 100
|
|
|
|
payouts = (
|
|
PaymentTransaction.objects
|
|
.filter(
|
|
payment_type='debit',
|
|
payment_transaction_status='completed',
|
|
payment_transaction_date=day,
|
|
)
|
|
.aggregate(total=Sum('payment_transaction_amount'))['total'] or 0
|
|
)
|
|
|
|
result.append({
|
|
'day': day.strftime('%a'),
|
|
'revenue': float(revenue),
|
|
'payouts': float(payouts),
|
|
})
|
|
|
|
return Response(result)
|
|
|
|
|
|
class DashboardActivityView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
from partner.models import Partner
|
|
from events.models import Event
|
|
from bookings.models import Booking
|
|
from django.utils import timezone
|
|
|
|
items = []
|
|
|
|
# --- Partners (last 5 by id desc; no date field — timestamp=None, filtered out later) ---
|
|
for p in Partner.objects.order_by('-id')[:5]:
|
|
items.append({
|
|
'id': f'partner-{p.id}',
|
|
'type': 'partner',
|
|
'title': f'{p.name} registered',
|
|
'description': f'New partner — {getattr(p, "partner_type", "individual")}',
|
|
'timestamp': None,
|
|
'status': p.kyc_compliance_status,
|
|
})
|
|
|
|
# --- Events (last 5 by created_date desc) ---
|
|
for e in Event.objects.order_by('-created_date')[:5]:
|
|
display_name = e.title if getattr(e, 'title', None) else getattr(e, 'name', '')
|
|
ts = e.created_date
|
|
items.append({
|
|
'id': f'event-{e.id}',
|
|
'type': 'event',
|
|
'title': f'{display_name} created',
|
|
'description': f'Event status: {e.event_status}',
|
|
'timestamp': ts.isoformat() if ts else None,
|
|
'status': e.event_status,
|
|
})
|
|
|
|
# --- Bookings (last 5 by id desc) ---
|
|
for b in Booking.objects.order_by('-id')[:5]:
|
|
ts = b.created_date
|
|
items.append({
|
|
'id': f'booking-{b.booking_id}',
|
|
'type': 'booking',
|
|
'title': f'Booking {b.booking_id} placed',
|
|
'description': f'{b.quantity} ticket(s) at ₹{b.price}',
|
|
'timestamp': ts.isoformat() if ts else None,
|
|
'status': 'confirmed',
|
|
})
|
|
|
|
# Filter out items with no timestamp, sort desc, return top 10
|
|
dated = [item for item in items if item['timestamp'] is not None]
|
|
dated.sort(key=lambda x: x['timestamp'], reverse=True)
|
|
|
|
return Response(dated[:10])
|
|
|
|
|
|
class DashboardActionsView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
from partner.models import Partner
|
|
from events.models import Event
|
|
from banking_operations.models import PaymentTransaction
|
|
from django.db.models import Sum
|
|
|
|
kyc_count = Partner.objects.filter(kyc_compliance_status='pending').count()
|
|
flagged_count = Event.objects.filter(event_status='flagged').count()
|
|
pending_payouts = float(
|
|
PaymentTransaction.objects
|
|
.filter(payment_type='debit', payment_transaction_status='pending')
|
|
.aggregate(total=Sum('payment_transaction_amount'))['total'] or 0
|
|
)
|
|
|
|
actions = [
|
|
{
|
|
'id': 'kyc',
|
|
'type': 'kyc',
|
|
'count': kyc_count,
|
|
'title': 'Partner Approval Queue',
|
|
'description': f'{kyc_count} partners awaiting KYC review',
|
|
'href': '/partners?filter=pending',
|
|
'priority': 'high',
|
|
},
|
|
{
|
|
'id': 'flagged',
|
|
'type': 'flagged',
|
|
'count': flagged_count,
|
|
'title': 'Flagged Events',
|
|
'description': f'{flagged_count} events reported for review',
|
|
'href': '/events?filter=flagged',
|
|
'priority': 'high',
|
|
},
|
|
{
|
|
'id': 'payout',
|
|
'type': 'payout',
|
|
'count': pending_payouts,
|
|
'title': 'Pending Payouts',
|
|
'description': f'₹{pending_payouts:,.0f} ready for release',
|
|
'href': '/financials?tab=payouts',
|
|
'priority': 'medium',
|
|
},
|
|
]
|
|
|
|
return Response(actions)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 3: Partner helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_PARTNER_STATUS_MAP = {
|
|
'active': 'Active', 'pending': 'Invited', 'suspended': 'Suspended',
|
|
'archived': 'Archived', 'deleted': 'Archived', 'inactive': 'Archived',
|
|
}
|
|
_PARTNER_KYC_MAP = {'approved': 'Verified', 'rejected': 'Rejected'}
|
|
_PARTNER_TYPE_MAP = {
|
|
'venue': 'Venue', 'promoter': 'Promoter', 'sponsor': 'Sponsor',
|
|
'vendor': 'Vendor', 'affiliate': 'Affiliate', 'other': 'Other',
|
|
}
|
|
_RISK_MAP = {
|
|
'high_risk': 80, 'medium_risk': 45, 'low_risk': 15,
|
|
'rejected': 90, 'pending': 30, 'approved': 5,
|
|
}
|
|
|
|
def _partner_kyc_docs(p):
|
|
if not p.kyc_compliance_document_type:
|
|
return []
|
|
return [{
|
|
'id': f'kyc-{p.id}',
|
|
'partnerId': str(p.id),
|
|
'type': p.kyc_compliance_document_type.upper(),
|
|
'name': p.kyc_compliance_document_other_type or p.kyc_compliance_document_type,
|
|
'url': p.kyc_compliance_document_file.url if p.kyc_compliance_document_file else '',
|
|
'status': {'approved': 'APPROVED', 'rejected': 'REJECTED'}.get(p.kyc_compliance_status, 'PENDING'),
|
|
'mandatory': True,
|
|
'adminNote': p.kyc_compliance_reason or '',
|
|
'uploadedBy': p.primary_contact_person_name,
|
|
'uploadedAt': '',
|
|
}]
|
|
|
|
def _serialize_partner(p, events_count=0):
|
|
addr = ', '.join(filter(None, [p.address, p.city, p.state, p.country]))
|
|
return {
|
|
'id': str(p.id),
|
|
'name': p.name,
|
|
'type': _PARTNER_TYPE_MAP.get(p.partner_type, 'Other'),
|
|
'status': _PARTNER_STATUS_MAP.get(p.status, 'Invited'),
|
|
'primaryContact': {
|
|
'name': p.primary_contact_person_name,
|
|
'email': p.primary_contact_person_email,
|
|
'phone': p.primary_contact_person_phone,
|
|
},
|
|
'companyDetails': {
|
|
'website': p.website_url or '',
|
|
'address': addr,
|
|
},
|
|
'metrics': {
|
|
'eventsCount': events_count,
|
|
'totalRevenue': 0,
|
|
'openBalance': 0,
|
|
'activeDeals': 0,
|
|
'lastActivity': None,
|
|
},
|
|
'verificationStatus': _PARTNER_KYC_MAP.get(p.kyc_compliance_status, 'Pending'),
|
|
'kycComplianceStatus': p.kyc_compliance_status,
|
|
'riskScore': _RISK_MAP.get(p.kyc_compliance_status, 0),
|
|
'joinedAt': None,
|
|
'tags': [],
|
|
'notes': p.kyc_compliance_reason or '',
|
|
'kycDocuments': _partner_kyc_docs(p),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 3: Partner Views
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class PartnerStatsView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
from partner.models import Partner
|
|
return Response({
|
|
'total': Partner.objects.count(),
|
|
'active': Partner.objects.filter(status='active').count(),
|
|
'pendingKyc': Partner.objects.filter(kyc_compliance_status='pending').count(),
|
|
'highRisk': Partner.objects.filter(kyc_compliance_status='high_risk').count(),
|
|
})
|
|
|
|
|
|
class PartnerListView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
from partner.models import Partner
|
|
from django.db.models import Count, Q
|
|
qs = Partner.objects.annotate(events_count=Count('event'))
|
|
if s := request.GET.get('status'): qs = qs.filter(status=s)
|
|
if k := request.GET.get('kyc_status'): qs = qs.filter(kyc_compliance_status=k)
|
|
if t := request.GET.get('partner_type'): qs = qs.filter(partner_type=t)
|
|
if q := request.GET.get('search'):
|
|
qs = qs.filter(
|
|
Q(name__icontains=q) |
|
|
Q(primary_contact_person_email__icontains=q) |
|
|
Q(primary_contact_person_name__icontains=q)
|
|
)
|
|
page = max(1, int(request.GET.get('page', 1)))
|
|
page_size = min(100, int(request.GET.get('page_size', 20)))
|
|
total = qs.count()
|
|
partners = qs.order_by('-id')[(page - 1) * page_size: page * page_size]
|
|
return Response({
|
|
'count': total,
|
|
'results': [_serialize_partner(p, p.events_count) for p in partners],
|
|
})
|
|
|
|
|
|
class PartnerDetailView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, pk):
|
|
from partner.models import Partner
|
|
from events.models import Event
|
|
from django.shortcuts import get_object_or_404
|
|
p = get_object_or_404(Partner, pk=pk)
|
|
events_count = Event.objects.filter(partner_id=pk).count()
|
|
data = _serialize_partner(p, events_count)
|
|
_EVENT_STATUS_MAP = {
|
|
'live': 'LIVE', 'published': 'LIVE', 'draft': 'DRAFT',
|
|
'cancelled': 'CANCELLED', 'flagged': 'PENDING_REVIEW', 'pending': 'PENDING_REVIEW',
|
|
}
|
|
events_qs = Event.objects.filter(partner_id=pk).order_by('-id')[:20]
|
|
data['events'] = [{
|
|
'id': str(e.id),
|
|
'partnerId': str(pk),
|
|
'title': e.title or e.name or '',
|
|
'date': e.start_date.isoformat() if e.start_date else '',
|
|
'venue': e.venue_name or '',
|
|
'category': '',
|
|
'ticketPrice': 0,
|
|
'totalTickets': 0,
|
|
'ticketsSold': 0,
|
|
'revenue': 0,
|
|
'status': _EVENT_STATUS_MAP.get(e.event_status, 'DRAFT'),
|
|
'submittedAt': e.created_date.isoformat() if e.created_date else '',
|
|
'createdAt': e.created_date.isoformat() if e.created_date else '',
|
|
'rejectionReason': '',
|
|
} for e in events_qs]
|
|
data['dealTerms'] = []
|
|
data['ledger'] = []
|
|
return Response(data)
|
|
|
|
|
|
class PartnerStatusView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def patch(self, request, pk):
|
|
from partner.models import Partner
|
|
from django.shortcuts import get_object_or_404
|
|
p = get_object_or_404(Partner, pk=pk)
|
|
new_status = request.data.get('status')
|
|
valid = ('active', 'suspended', 'inactive', 'archived')
|
|
if new_status not in valid:
|
|
return Response({'error': f'status must be one of {valid}'}, status=400)
|
|
p.status = new_status
|
|
p.kyc_compliance_reason = request.data.get('reason', p.kyc_compliance_reason or '')
|
|
p.save(update_fields=['status', 'kyc_compliance_reason'])
|
|
return Response({'id': str(p.id), 'status': p.status})
|
|
|
|
|
|
class PartnerKYCReviewView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request, pk):
|
|
from partner.models import Partner
|
|
from django.shortcuts import get_object_or_404
|
|
p = get_object_or_404(Partner, pk=pk)
|
|
decision = request.data.get('decision')
|
|
if decision not in ('approved', 'rejected'):
|
|
return Response({'error': 'decision must be approved or rejected'}, status=400)
|
|
p.kyc_compliance_status = decision
|
|
p.is_kyc_compliant = (decision == 'approved')
|
|
p.kyc_compliance_reason = request.data.get('reason', '')
|
|
p.save(update_fields=['kyc_compliance_status', 'is_kyc_compliant', 'kyc_compliance_reason'])
|
|
return Response({
|
|
'id': str(p.id),
|
|
'kyc_compliance_status': p.kyc_compliance_status,
|
|
'is_kyc_compliant': p.is_kyc_compliant,
|
|
'verificationStatus': 'Verified' if decision == 'approved' else 'Rejected',
|
|
})
|