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', }) # --------------------------------------------------------------------------- # Phase 4: Users API # --------------------------------------------------------------------------- def _user_status(u): return 'Active' if u.is_active else 'Suspended' _USER_ROLE_MAP = { 'admin': 'Admin', 'manager': 'Admin', 'staff': 'Support Agent', 'customer': 'User', 'is_user': 'User', 'partner': 'Partner', 'partner_manager': 'Partner', 'partner_staff': 'Partner', 'partner_customer': 'Partner', } def _serialize_user(u): full_name = f'{u.first_name} {u.last_name}'.strip() or u.username role_key = u.role if u.role else ('customer' if getattr(u, 'is_customer', False) else 'staff') try: pic = u.profile_picture avatar = pic.url if pic and pic.name and pic.name != 'default.png' else '' except Exception: avatar = '' return { 'id': str(u.id), 'name': full_name, 'email': u.email, 'phone': getattr(u, 'phone_number', '') or '', 'countryCode': '+91', 'avatarUrl': avatar, 'role': _USER_ROLE_MAP.get(role_key, 'User'), 'status': _user_status(u), 'tier': 'Bronze', 'healthScore': 'warm', 'isVerified': u.is_active, 'is2FAEnabled': False, 'totalSpent': 0, 'bookingsCount': 0, 'refundRate': 0, 'averageOrderValue': 0, 'language': 'en', 'timezone': 'Asia/Kolkata', 'currency': 'INR', 'emailNotifications': True, 'pushNotifications': True, 'smsNotifications': True, 'tags': [], 'pinnedNote': '', 'createdAt': u.date_joined.isoformat() if u.date_joined else '', 'updatedAt': u.date_joined.isoformat() if u.date_joined else '', 'lastLoginAt': u.last_login.isoformat() if u.last_login else '', 'lastActivityAt': u.last_login.isoformat() if u.last_login else '', } class UserMetricsView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from django.contrib.auth import get_user_model from django.db.models import Q from django.utils import timezone import datetime User = get_user_model() today = timezone.now().date() week_ago = today - datetime.timedelta(days=7) # Customers = all non-superuser accounts (end users registered via mobile/web) customer_qs = User.objects.filter(is_superuser=False) return Response({ 'total': customer_qs.count(), 'active': customer_qs.filter(is_active=True).count(), 'suspended': customer_qs.filter(is_active=False).count(), 'banned': 0, # Reserved for future explicit ban field 'newThisWeek': customer_qs.filter(date_joined__date__gte=week_ago).count(), }) class UserListView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from django.contrib.auth import get_user_model from django.db.models import Q User = get_user_model() qs = User.objects.filter(is_superuser=False) # Server-side search search = request.query_params.get('search', '').strip() if search: qs = qs.filter( Q(first_name__icontains=search) | Q(last_name__icontains=search) | Q(email__icontains=search) | Q(username__icontains=search) | Q(phone_number__icontains=search) ) # Status filter status_filter = request.query_params.get('status', '').strip().lower() if status_filter == 'active': qs = qs.filter(is_active=True) elif status_filter == 'suspended': qs = qs.filter(is_active=False) # Role filter role_filter = request.query_params.get('role', '').strip() if role_filter: qs = qs.filter(role__iexact=role_filter) try: page = max(1, int(request.query_params.get('page', 1))) page_size = min(100, int(request.query_params.get('page_size', 20))) except (ValueError, TypeError): page, page_size = 1, 20 total = qs.count() users = qs.order_by('-date_joined')[(page - 1) * page_size: page * page_size] return Response({'count': total, 'results': [_serialize_user(u) for u in users]}) class UserDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 User = get_user_model() u = get_object_or_404(User, pk=pk) return Response(_serialize_user(u)) class UserStatusView(APIView): permission_classes = [IsAuthenticated] def patch(self, request, pk): from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 User = get_user_model() u = get_object_or_404(User, pk=pk) action = request.data.get('action') if action in ('suspend', 'ban'): u.is_active = False elif action == 'reinstate': u.is_active = True else: return Response({'error': 'action must be suspend, ban, or reinstate'}, status=400) u.save(update_fields=['is_active']) return Response({'id': str(u.id), 'status': _user_status(u)}) # --------------------------------------------------------------------------- # Phase 5: Events API # --------------------------------------------------------------------------- _EVENT_STATUS_MAP = { 'live': 'live', 'published': 'published', 'pending': 'published', 'created': 'draft', 'flagged': 'flagged', 'cancelled': 'cancelled', 'postponed': 'cancelled', 'completed': 'completed', } def _serialize_event(e): partner_name = '' if e.partner_id: try: partner_name = e.partner.name except Exception: partner_name = '' return { 'id': str(e.id), 'title': e.title or getattr(e, 'name', '') or '', 'partnerId': str(e.partner_id) if e.partner_id else '', 'partnerName': partner_name, 'date': e.start_date.isoformat() if e.start_date else '', 'status': _EVENT_STATUS_MAP.get(e.event_status, 'draft'), 'ticketsSold': 0, 'revenue': 0, 'venueName': e.venue_name or '', 'createdAt': e.created_date.isoformat() if e.created_date else '', 'isFeatured': bool(e.is_featured), 'isTopEvent': bool(e.is_top_event), } def _serialize_event_detail(e): """Full event serializer for detail view -- includes all fields + images.""" base = _serialize_event(e) base.update({ 'name': e.name or '', 'description': e.description or '', 'endDate': e.end_date.isoformat() if e.end_date else '', 'startTime': str(e.start_time or ''), 'endTime': str(e.end_time or ''), 'allYearEvent': bool(e.all_year_event), 'latitude': str(e.latitude) if e.latitude else '', 'longitude': str(e.longitude) if e.longitude else '', 'pincode': e.pincode or '', 'district': e.district or '', 'state': e.state or '', 'place': e.place or '', 'isBookable': bool(e.is_bookable), 'eventType': e.event_type_id, 'importantInformation': e.important_information or '', 'source': e.source or '', 'cancelledReason': e.cancelled_reason or '', 'outsideEventUrl': e.outside_event_url or '', 'images': [ { 'id': img.id, 'url': f'/media/{img.event_image}', 'isPrimary': bool(img.is_primary), } for img in e.eventimages_set.all() ], }) return base class EventStatsView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from events.models import Event return Response({ 'total': Event.objects.count(), 'live': Event.objects.filter(event_status='live').count(), 'pending': Event.objects.filter(event_status__in=['pending', 'created']).count(), 'flagged': Event.objects.filter(event_status='flagged').count(), 'published': Event.objects.filter(event_status='published').count(), }) class EventListView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from events.models import Event from django.db.models import Q qs = Event.objects.select_related('partner').all() if s := request.GET.get('status'): reverse_map = {v: k for k, v in _EVENT_STATUS_MAP.items()} backend_status = reverse_map.get(s, s) qs = qs.filter(event_status=backend_status) if pid := request.GET.get('partner_id'): qs = qs.filter(partner_id=pid) if q := request.GET.get('search'): qs = qs.filter( Q(title__icontains=q) | Q(name__icontains=q) | Q(venue_name__icontains=q) ) try: page = max(1, int(request.GET.get('page', 1))) page_size = min(100, int(request.GET.get('page_size', 20))) except (ValueError, TypeError): page, page_size = 1, 20 total = qs.count() events = qs.order_by('-id')[(page - 1) * page_size: page * page_size] return Response({'count': total, 'results': [_serialize_event(e) for e in events]}) class EventDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): from events.models import Event from django.shortcuts import get_object_or_404 e = get_object_or_404(Event.objects.select_related('partner').prefetch_related('eventimages_set'), pk=pk) return Response(_serialize_event_detail(e)) class EventUpdateView(APIView): permission_classes = [IsAuthenticated] def patch(self, request, pk): from events.models import Event from django.shortcuts import get_object_or_404 e = get_object_or_404(Event, pk=pk) field_map = { 'title': 'title', 'name': 'name', 'description': 'description', 'venueName': 'venue_name', 'place': 'place', 'district': 'district', 'state': 'state', 'pincode': 'pincode', 'importantInformation': 'important_information', 'source': 'source', 'cancelledReason': 'cancelled_reason', 'outsideEventUrl': 'outside_event_url', } bool_fields = { 'isBookable': 'is_bookable', 'isFeatured': 'is_featured', 'isTopEvent': 'is_top_event', 'allYearEvent': 'all_year_event', } updated_fields = [] for api_key, model_field in field_map.items(): if api_key in request.data: setattr(e, model_field, request.data[api_key] or '') updated_fields.append(model_field) for api_key, model_field in bool_fields.items(): if api_key in request.data: setattr(e, model_field, bool(request.data[api_key])) updated_fields.append(model_field) # Handle status if 'status' in request.data: reverse_map = {v: k for k, v in _EVENT_STATUS_MAP.items()} backend_status = reverse_map.get(request.data['status'], request.data['status']) e.event_status = backend_status updated_fields.append('event_status') if updated_fields: e.save(update_fields=updated_fields) # Re-fetch with relations for response e = Event.objects.select_related('partner').prefetch_related('eventimages_set').get(pk=pk) return Response(_serialize_event_detail(e)) class EventModerationView(APIView): permission_classes = [IsAuthenticated] def patch(self, request, pk): from events.models import Event from django.shortcuts import get_object_or_404 e = get_object_or_404(Event, pk=pk) action = request.data.get('action') if action == 'approve': e.event_status = 'published' e.save(update_fields=['event_status']) elif action == 'reject': e.event_status = 'cancelled' e.cancelled_reason = request.data.get('reason', '') e.save(update_fields=['event_status', 'cancelled_reason']) elif action == 'flag': e.event_status = 'flagged' e.save(update_fields=['event_status']) elif action == 'feature': e.is_featured = True e.save(update_fields=['is_featured']) elif action == 'unfeature': e.is_featured = False e.save(update_fields=['is_featured']) else: return Response({'error': 'Invalid action'}, status=400) e.refresh_from_db() return Response(_serialize_event(e)) # --------------------------------------------------------------------------- # Phase 6: Financials & Payouts # --------------------------------------------------------------------------- _TX_STATUS_MAP = { 'captured': 'Completed', 'created': 'Pending', 'failed': 'Failed', 'refunded': 'Failed', } def _serialize_transaction(t): ts = t.captured_at or t.created_at order_ref = getattr(t, 'razorpay_order_id', None) or getattr(t, 'transaction_id', None) return { 'id': str(t.id), 'title': f'Payment {order_ref}' if order_ref else f'Transaction #{t.id}', 'partner': '', 'amount': t.amount / 100, 'date': ts.isoformat() if ts else '', 'type': 'in', 'method': 'Razorpay', 'fees': 0, 'net': t.amount / 100, 'status': _TX_STATUS_MAP.get(t.status, 'Pending'), } _SETTLEMENT_STATUS_MAP = { 'pending': 'Ready', 'failed': 'Overdue', 'cancelled': 'Overdue', 'completed': 'On Hold', 'refunded': 'On Hold', } def _serialize_settlement(p): return { 'id': str(p.id), 'partnerName': '', 'eventName': '', 'amount': float(p.payment_transaction_amount), 'dueDate': p.payment_transaction_date.isoformat() if p.payment_transaction_date else '', 'status': _SETTLEMENT_STATUS_MAP.get(p.payment_transaction_status, 'On Hold'), } class FinancialMetricsView(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 total_paise = ( RazorpayTransaction.objects.filter(status='captured') .aggregate(t=Sum('amount'))['t'] or 0 ) total_revenue = total_paise / 100 total_payouts = float( PaymentTransaction.objects .filter(payment_type='debit', payment_transaction_status='completed') .aggregate(t=Sum('payment_transaction_amount'))['t'] or 0 ) platform_earnings = round(total_revenue * 0.12, 2) pending_count = PaymentTransaction.objects.filter( payment_type='debit', payment_transaction_status='pending' ).count() pending_amount = float( PaymentTransaction.objects .filter(payment_type='debit', payment_transaction_status='pending') .aggregate(t=Sum('payment_transaction_amount'))['t'] or 0 ) return Response({ 'totalRevenue': total_revenue, 'totalPayouts': total_payouts, 'platformEarnings': platform_earnings, 'pendingPayouts': pending_count, 'pendingPayoutAmount': pending_amount, }) class TransactionListView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from ledger.models import RazorpayTransaction try: page = max(1, int(request.GET.get('page', 1))) page_size = min(100, int(request.GET.get('page_size', 20))) except (ValueError, TypeError): page, page_size = 1, 20 qs = RazorpayTransaction.objects.order_by('-id') total = qs.count() txs = qs[(page - 1) * page_size: page * page_size] return Response({'count': total, 'results': [_serialize_transaction(t) for t in txs]}) class SettlementListView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from banking_operations.models import PaymentTransaction qs = PaymentTransaction.objects.filter( payment_type='debit' ).order_by('-id')[:50] return Response([_serialize_settlement(p) for p in qs]) class SettlementReleaseView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): from banking_operations.models import PaymentTransaction from django.shortcuts import get_object_or_404 p = get_object_or_404(PaymentTransaction, pk=pk, payment_type='debit') p.payment_transaction_status = 'completed' p.save(update_fields=['payment_transaction_status']) return Response(_serialize_settlement(p)) # --------------------------------------------------------------------------- # Phase 7: Reviews Moderation # --------------------------------------------------------------------------- def _reviewer_rank(total_reviews): if total_reviews >= 41: return 'Legend' if total_reviews >= 21: return 'Champion' if total_reviews >= 11: return 'Enthusiast' if total_reviews >= 4: return 'Contributor' return 'Explorer' def _serialize_review(r): from admin_api.models import Review try: name = r.reviewer.get_full_name() or r.reviewer.username email = r.reviewer.email total = Review.objects.filter(reviewer=r.reviewer, status='live').count() except Exception: name, email, total = '', '', 0 try: event_name = getattr(r.event, 'title', None) or getattr(r.event, 'name', '') or '' event_id = str(r.event.id) start = getattr(r.event, 'start_date', None) event_date = start.isoformat() if start else '' except Exception: event_name, event_id, event_date = '', '', '' return { 'id': str(r.id), 'reviewerName': name, 'reviewerEmail': email, 'reviewerAvatar': '', 'eventName': event_name, 'eventId': event_id, 'eventDate': event_date, 'rating': r.rating, 'reviewText': r.review_text, 'submissionDate': r.submission_date.isoformat(), 'status': r.status, 'reviewerRank': _reviewer_rank(total), 'reviewerTotalReviews': total, 'rejectReason': r.reject_reason or '', } class ReviewMetricsView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from admin_api.models import Review return Response({ 'totalPending': Review.objects.filter(status='pending').count(), 'liveReviews': Review.objects.filter(status='live').count(), 'rejected': Review.objects.filter(status='rejected').count(), }) class ReviewListView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from admin_api.models import Review qs = Review.objects.select_related('reviewer', 'event').order_by('-submission_date') status = request.GET.get('status') if status in ('pending', 'live', 'rejected'): qs = qs.filter(status=status) try: page = max(1, int(request.GET.get('page', 1))) page_size = min(100, int(request.GET.get('page_size', 20))) except (ValueError, TypeError): page, page_size = 1, 20 total = qs.count() reviews = qs[(page - 1) * page_size: page * page_size] return Response({'count': total, 'results': [_serialize_review(r) for r in reviews]}) class ReviewModerationView(APIView): permission_classes = [IsAuthenticated] def patch(self, request, pk): from admin_api.models import Review from django.shortcuts import get_object_or_404 review = get_object_or_404(Review, pk=pk) action = request.data.get('action') if action == 'approve': review.status = 'live' elif action == 'reject': rr = request.data.get('reject_reason', 'spam') valid_reasons = [c[0] for c in Review.REJECT_CHOICES] if rr not in valid_reasons: return Response({'error': 'Invalid reject_reason'}, status=400) review.status = 'rejected' review.reject_reason = rr elif action == 'save_and_approve': review.review_text = request.data.get('review_text', review.review_text) review.status = 'live' elif action == 'save_live': review.review_text = request.data.get('review_text', review.review_text) else: return Response({'error': 'Invalid action'}, status=400) review.save() return Response(_serialize_review(review)) class ReviewDeleteView(APIView): permission_classes = [IsAuthenticated] def delete(self, request, pk): from admin_api.models import Review from django.shortcuts import get_object_or_404 review = get_object_or_404(Review, pk=pk) review.delete() return Response(status=204) # --- Payment Gateway Settings --- def _serialize_gateway(gw, include_secret=False): data = { 'id': gw.pk, 'payment_gateway_id': gw.payment_gateway_id, 'name': gw.payment_gateway_name, 'description': gw.payment_gateway_description, 'url': gw.payment_gateway_url, 'api_key': gw.payment_gateway_api_key, 'api_url': gw.payment_gateway_api_url, 'api_version': gw.payment_gateway_api_version, 'api_method': gw.payment_gateway_api_method, 'is_active': gw.is_active, 'gateway_priority': gw.gateway_priority, 'created_date': gw.created_date.isoformat() if gw.created_date else None, 'updated_date': gw.updated_date.isoformat() if gw.updated_date else None, 'logo': gw.payment_gateway_logo.url if gw.payment_gateway_logo else None, } if include_secret: data['api_secret'] = gw.payment_gateway_api_secret return data class ActivePaymentGatewayView(APIView): permission_classes = [AllowAny] def get(self, request): from banking_operations.models import PaymentGateway gateway = PaymentGateway.objects.filter(is_active=True).order_by('-gateway_priority', '-id').first() if not gateway: return Response({'status': 'error', 'message': 'No active payment gateway configured.'}, status=404) return Response({ 'status': 'success', 'gateway': { 'name': gateway.payment_gateway_name, 'key_id': gateway.payment_gateway_api_key, 'currency': 'INR', } }) class PaymentGatewaySettingsView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk=None): from banking_operations.models import PaymentGateway gateways = PaymentGateway.objects.all().order_by('-gateway_priority', '-id') return Response({ 'status': 'success', 'payment_gateways': [_serialize_gateway(g, include_secret=True) for g in gateways] }) def post(self, request, pk=None): from banking_operations.models import PaymentGateway import uuid d = request.data required = ['name', 'api_key', 'api_secret'] missing = [f for f in required if not d.get(f)] if missing: return Response({'status': 'error', 'message': 'Missing fields: {}'.format(missing)}, status=400) gw = PaymentGateway.objects.create( payment_gateway_id=str(uuid.uuid4().hex[:10]).upper(), payment_gateway_name=d['name'], payment_gateway_description=d.get('description', ''), payment_gateway_url=d.get('url', ''), payment_gateway_api_key=d['api_key'], payment_gateway_api_secret=d['api_secret'], payment_gateway_api_url=d.get('api_url', ''), payment_gateway_api_version=d.get('api_version', 'v1'), payment_gateway_api_method=d.get('api_method', 'POST'), is_active=d.get('is_active', True), gateway_priority=int(d.get('gateway_priority', 0)), ) return Response({'status': 'success', 'payment_gateway': _serialize_gateway(gw, include_secret=True)}, status=201) def patch(self, request, pk=None): from banking_operations.models import PaymentGateway from django.shortcuts import get_object_or_404 gw = get_object_or_404(PaymentGateway, pk=pk) d = request.data field_map = { 'name': 'payment_gateway_name', 'description': 'payment_gateway_description', 'url': 'payment_gateway_url', 'api_key': 'payment_gateway_api_key', 'api_secret': 'payment_gateway_api_secret', 'api_url': 'payment_gateway_api_url', 'api_version': 'payment_gateway_api_version', 'api_method': 'payment_gateway_api_method', 'is_active': 'is_active', 'gateway_priority': 'gateway_priority', } for client_field, model_field in field_map.items(): if client_field in d: setattr(gw, model_field, d[client_field]) gw.save() return Response({'status': 'success', 'payment_gateway': _serialize_gateway(gw, include_secret=True)}) def delete(self, request, pk=None): from banking_operations.models import PaymentGateway from django.shortcuts import get_object_or_404 gw = get_object_or_404(PaymentGateway, pk=pk) gw.delete() return Response({'status': 'success'}, status=200) class EventCreateView(APIView): permission_classes = [IsAuthenticated] def post(self, request): from events.models import Event, EventType data = request.data # Required fields title = (data.get('title') or '').strip() name = (data.get('name') or title).strip() if not title: return Response({'error': 'Title is required'}, status=400) # Get event_type (required FK) event_type_id = data.get('eventType') if not event_type_id: return Response({'error': 'Event type is required'}, status=400) try: event_type = EventType.objects.get(id=event_type_id) except EventType.DoesNotExist: return Response({'error': 'Invalid event type'}, status=400) # Build the event event = Event( title=title, name=name, description=data.get('description', ''), event_type=event_type, event_status=data.get('eventStatus', 'pending'), venue_name=data.get('venueName', ''), place=data.get('place', ''), district=data.get('district', ''), state=data.get('state', ''), pincode=data.get('pincode', ''), latitude=data.get('latitude', 0), longitude=data.get('longitude', 0), is_bookable=data.get('isBookable', False), is_featured=data.get('isFeatured', False), is_top_event=data.get('isTopEvent', False), all_year_event=data.get('allYearEvent', False), source=data.get('source', 'official'), important_information=data.get('importantInformation', ''), cancelled_reason=data.get('cancelledReason', 'NA'), outside_event_url=data.get('outsideEventUrl', 'NA'), is_eventify_event=data.get('isEventifyEvent', True), ) # Optional dates/times if data.get('startDate'): event.start_date = data['startDate'] if data.get('endDate'): event.end_date = data['endDate'] if data.get('startTime'): event.start_time = data['startTime'] if data.get('endTime'): event.end_time = data['endTime'] # Optional partner partner_id = data.get('partnerId') if partner_id: try: from partners.models import Partner event.partner = Partner.objects.get(id=partner_id) event.is_partner_event = True except Exception: pass event.save() return Response(_serialize_event_detail(event), status=201) class EventTypesView(APIView): permission_classes = [IsAuthenticated] def get(self, request): from events.models import EventType types = EventType.objects.all().order_by('id') return Response([ {'id': t.id, 'name': t.event_type} for t in types ]) class EventPrimaryImageView(APIView): permission_classes = [IsAuthenticated] def patch(self, request, pk): from events.models import Event, EventImages try: event = Event.objects.get(pk=pk) except Event.DoesNotExist: return Response({"error": "Event not found"}, status=404) image_id = request.data.get("image_id") if not image_id: return Response({"error": "image_id is required"}, status=400) try: img = EventImages.objects.get(pk=image_id, event=event) except EventImages.DoesNotExist: return Response({"error": "Image not found for this event"}, status=404) # Clear all primary flags for this event, then set the selected one EventImages.objects.filter(event=event).update(is_primary=False) img.is_primary = True img.save() return Response({"success": True, "primaryImageId": image_id})