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
This commit is contained in:
Ubuntu
2026-03-24 18:11:33 +00:00
parent b60d03142c
commit cbe06e9c8f
2 changed files with 191 additions and 0 deletions

View File

@@ -12,4 +12,10 @@ urlpatterns = [
path('dashboard/revenue/', views.DashboardRevenueView.as_view(), name='dashboard-revenue'), path('dashboard/revenue/', views.DashboardRevenueView.as_view(), name='dashboard-revenue'),
path('dashboard/activity/', views.DashboardActivityView.as_view(), name='dashboard-activity'), path('dashboard/activity/', views.DashboardActivityView.as_view(), name='dashboard-activity'),
path('dashboard/actions/', views.DashboardActionsView.as_view(), name='dashboard-actions'), 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/<int:pk>/', views.PartnerDetailView.as_view(), name='partner-detail'),
path('partners/<int:pk>/status/', views.PartnerStatusView.as_view(), name='partner-status'),
path('partners/<int:pk>/kyc/review/', views.PartnerKYCReviewView.as_view(), name='partner-kyc-review'),
] ]

View File

@@ -286,3 +286,188 @@ class DashboardActionsView(APIView):
] ]
return Response(actions) 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',
})