From cbe06e9c8ffe8b888aedb1780087c4e84ae2a0aa Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 24 Mar 2026 18:11:33 +0000 Subject: [PATCH] feat: Phase 3 - Partners API (5 endpoints + 2 helpers) - 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 --- admin_api/urls.py | 6 ++ admin_api/views.py | 185 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) diff --git a/admin_api/urls.py b/admin_api/urls.py index a571f69..f36e41a 100644 --- a/admin_api/urls.py +++ b/admin_api/urls.py @@ -12,4 +12,10 @@ urlpatterns = [ path('dashboard/revenue/', views.DashboardRevenueView.as_view(), name='dashboard-revenue'), path('dashboard/activity/', views.DashboardActivityView.as_view(), name='dashboard-activity'), path('dashboard/actions/', views.DashboardActionsView.as_view(), name='dashboard-actions'), + # Phase 3: Partner endpoints + path('partners/stats/', views.PartnerStatsView.as_view(), name='partner-stats'), + path('partners/', views.PartnerListView.as_view(), name='partner-list'), + path('partners//', views.PartnerDetailView.as_view(), name='partner-detail'), + path('partners//status/', views.PartnerStatusView.as_view(), name='partner-status'), + path('partners//kyc/review/', views.PartnerKYCReviewView.as_view(), name='partner-kyc-review'), ] diff --git a/admin_api/views.py b/admin_api/views.py index 45d9cd3..0707f61 100644 --- a/admin_api/views.py +++ b/admin_api/views.py @@ -286,3 +286,188 @@ class DashboardActionsView(APIView): ] 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', + })