The changes for the new
This commit is contained in:
331
partner/api.py
331
partner/api.py
@@ -1,15 +1,26 @@
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.forms.models import model_to_dict
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models as django_models
|
||||
from django.db.models import Sum, F, Max, Min, DecimalField
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime, date
|
||||
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from partner.models import Partner
|
||||
from partner.models import (
|
||||
Partner,
|
||||
PARTNER_TYPE_CHOICES,
|
||||
STATUS_CHOICES,
|
||||
KYC_DOCUMENT_TYPE_CHOICES,
|
||||
)
|
||||
from events.models import Event
|
||||
from bookings.models import TicketMeta, Booking, TicketType
|
||||
from mobile_api.utils import validate_token_and_get_user
|
||||
from eventify_logger.services import log
|
||||
|
||||
@@ -55,6 +66,265 @@ def _partner_to_dict(partner, request=None):
|
||||
return data
|
||||
|
||||
|
||||
def _iso_z_from_date(d):
|
||||
if not d:
|
||||
return None
|
||||
if isinstance(d, datetime):
|
||||
if timezone.is_naive(d):
|
||||
d = timezone.make_aware(d, timezone.get_current_timezone())
|
||||
return d.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
||||
if isinstance(d, date):
|
||||
return f"{d.isoformat()}T00:00:00.000Z"
|
||||
return None
|
||||
|
||||
|
||||
def _format_event_time(t):
|
||||
if not t:
|
||||
return None
|
||||
return t.strftime("%H:%M")
|
||||
|
||||
|
||||
def _partner_type_display(partner):
|
||||
return dict(PARTNER_TYPE_CHOICES).get(partner.partner_type, partner.partner_type)
|
||||
|
||||
|
||||
def _partner_status_display(partner):
|
||||
return dict(STATUS_CHOICES).get(partner.status, partner.status)
|
||||
|
||||
|
||||
def _partner_logo_url(partner):
|
||||
return (
|
||||
"https://ui-avatars.com/api/?name="
|
||||
f"{quote_plus(partner.name)}&background=0D8ABC&color=fff"
|
||||
)
|
||||
|
||||
|
||||
def _company_address_line(partner):
|
||||
parts = [
|
||||
partner.address,
|
||||
partner.city,
|
||||
partner.state,
|
||||
partner.pincode,
|
||||
partner.country,
|
||||
]
|
||||
return ", ".join(p.strip() for p in parts if p and str(p).strip()) or None
|
||||
|
||||
|
||||
def _verification_status_label(partner):
|
||||
if partner.is_kyc_compliant and partner.kyc_compliance_status == "approved":
|
||||
return "Verified"
|
||||
if partner.kyc_compliance_status == "rejected":
|
||||
return "Rejected"
|
||||
if partner.kyc_compliance_status == "pending":
|
||||
return "Pending"
|
||||
return partner.get_kyc_compliance_status_display()
|
||||
|
||||
|
||||
def _risk_score(partner):
|
||||
scores = {
|
||||
"high_risk": 78,
|
||||
"medium_risk": 45,
|
||||
"low_risk": 12,
|
||||
"approved": 12,
|
||||
"pending": 50,
|
||||
"rejected": 95,
|
||||
}
|
||||
return scores.get(partner.kyc_compliance_status, 50)
|
||||
|
||||
|
||||
def _kyc_status_upper(partner):
|
||||
mapping = {
|
||||
"approved": "APPROVED",
|
||||
"pending": "PENDING",
|
||||
"rejected": "REJECTED",
|
||||
"high_risk": "HIGH_RISK",
|
||||
"low_risk": "LOW_RISK",
|
||||
"medium_risk": "MEDIUM_RISK",
|
||||
}
|
||||
return mapping.get(partner.kyc_compliance_status, str(partner.kyc_compliance_status).upper())
|
||||
|
||||
|
||||
def _kyc_document_type_label(partner):
|
||||
if partner.kyc_compliance_document_type == "other" and partner.kyc_compliance_document_other_type:
|
||||
return partner.kyc_compliance_document_other_type
|
||||
if partner.kyc_compliance_document_type:
|
||||
return dict(KYC_DOCUMENT_TYPE_CHOICES).get(
|
||||
partner.kyc_compliance_document_type,
|
||||
partner.kyc_compliance_document_type,
|
||||
)
|
||||
return "Document"
|
||||
|
||||
|
||||
def _build_kyc_documents(partner, request):
|
||||
"""Single KYC row from Partner model fields (no separate KYC table)."""
|
||||
if not (
|
||||
partner.kyc_compliance_document_file
|
||||
or partner.kyc_compliance_document_type
|
||||
or partner.kyc_compliance_document_number
|
||||
):
|
||||
return []
|
||||
|
||||
type_label = _kyc_document_type_label(partner)
|
||||
name = f"{type_label} - {partner.name}"
|
||||
if partner.kyc_compliance_document_file:
|
||||
if request:
|
||||
url = request.build_absolute_uri(partner.kyc_compliance_document_file.url)
|
||||
else:
|
||||
url = partner.kyc_compliance_document_file.url
|
||||
else:
|
||||
url = None
|
||||
|
||||
return [
|
||||
{
|
||||
"id": f"kyc-{partner.id}",
|
||||
"partnerId": f"p{partner.id}",
|
||||
"type": type_label.upper() if type_label else "DOCUMENT",
|
||||
"name": name,
|
||||
"url": url,
|
||||
"status": _kyc_status_upper(partner),
|
||||
"mandatory": True,
|
||||
"reviewedBy": "Admin" if partner.kyc_compliance_status == "approved" else None,
|
||||
"reviewedAt": None,
|
||||
"uploadedBy": partner.primary_contact_person_name,
|
||||
"uploadedAt": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def _build_partner_detail_payload(partner, request):
|
||||
"""Shape partner block for PartnerDetailWithEventsAPI."""
|
||||
ptype = _partner_type_display(partner)
|
||||
events_qs = Event.objects.filter(partner=partner)
|
||||
events_count = events_qs.count()
|
||||
active_deals = events_qs.filter(event_status__in=["live", "published"]).count()
|
||||
|
||||
booking_agg = (
|
||||
Booking.objects.filter(ticket_meta__event__partner=partner)
|
||||
.aggregate(
|
||||
total_revenue=Sum(
|
||||
F("quantity") * F("price"),
|
||||
output_field=DecimalField(max_digits=16, decimal_places=2),
|
||||
),
|
||||
last_booking=Max("updated_date"),
|
||||
)
|
||||
)
|
||||
total_revenue = booking_agg["total_revenue"] or Decimal("0")
|
||||
last_booking_date = booking_agg["last_booking"]
|
||||
|
||||
last_event_date = events_qs.aggregate(m=Max("created_date"))["m"]
|
||||
last_activity = None
|
||||
for candidate in (last_booking_date, last_event_date):
|
||||
if candidate is None:
|
||||
continue
|
||||
if last_activity is None or candidate > last_activity:
|
||||
last_activity = candidate
|
||||
|
||||
tags = [ptype]
|
||||
if partner.is_kyc_compliant:
|
||||
tags.append("KYC Verified")
|
||||
|
||||
return {
|
||||
"id": f"p{partner.id}",
|
||||
"name": partner.name,
|
||||
"type": ptype,
|
||||
"status": _partner_status_display(partner),
|
||||
"logo": _partner_logo_url(partner),
|
||||
"primaryContact": {
|
||||
"name": partner.primary_contact_person_name,
|
||||
"email": partner.primary_contact_person_email,
|
||||
"phone": partner.primary_contact_person_phone,
|
||||
"role": f"{ptype} Manager",
|
||||
},
|
||||
"companyDetails": {
|
||||
"legalName": partner.name,
|
||||
"taxId": partner.kyc_compliance_document_number or None,
|
||||
"website": partner.website_url or None,
|
||||
"address": _company_address_line(partner),
|
||||
},
|
||||
"metrics": {
|
||||
"activeDeals": active_deals,
|
||||
"totalRevenue": float(total_revenue),
|
||||
"openBalance": 0,
|
||||
"lastActivity": _iso_z_from_date(last_activity),
|
||||
"eventsCount": events_count,
|
||||
},
|
||||
"tags": tags,
|
||||
"notes": partner.kyc_compliance_reason or None,
|
||||
"joinedAt": None,
|
||||
"verificationStatus": _verification_status_label(partner),
|
||||
"riskScore": _risk_score(partner),
|
||||
}
|
||||
|
||||
|
||||
def _build_partner_events_payload(partner, events, request):
|
||||
"""partnerEvents[] from Event + TicketMeta / Booking aggregates."""
|
||||
if not events:
|
||||
return []
|
||||
|
||||
event_ids = [e.id for e in events]
|
||||
|
||||
cap_by_event = dict(
|
||||
TicketMeta.objects.filter(event_id__in=event_ids)
|
||||
.values("event_id")
|
||||
.annotate(total=Sum("maximum_quantity"))
|
||||
.values_list("event_id", "total")
|
||||
)
|
||||
|
||||
booking_rows = (
|
||||
Booking.objects.filter(ticket_meta__event_id__in=event_ids)
|
||||
.values("ticket_meta__event_id")
|
||||
.annotate(
|
||||
sold=Sum("quantity"),
|
||||
revenue=Sum(
|
||||
F("quantity") * F("price"),
|
||||
output_field=DecimalField(max_digits=16, decimal_places=2),
|
||||
),
|
||||
)
|
||||
)
|
||||
booking_by_event = {row["ticket_meta__event_id"]: row for row in booking_rows}
|
||||
|
||||
min_price_rows = (
|
||||
TicketType.objects.filter(ticket_meta__event_id__in=event_ids, is_active=True)
|
||||
.values("ticket_meta__event_id")
|
||||
.annotate(mp=Min("price"))
|
||||
)
|
||||
min_price_by_event = {row["ticket_meta__event_id"]: row["mp"] for row in min_price_rows}
|
||||
|
||||
out = []
|
||||
for e in events:
|
||||
row = booking_by_event.get(e.id, {})
|
||||
sold = row.get("sold") or 0
|
||||
revenue = row.get("revenue") or Decimal("0")
|
||||
mp = min_price_by_event.get(e.id)
|
||||
ticket_price = float(mp) if mp is not None else 0.0
|
||||
total_tickets = cap_by_event.get(e.id) or 0
|
||||
|
||||
title = (e.title or "").strip() or e.name
|
||||
venue = (e.venue_name or "").strip() or e.place
|
||||
category = e.event_type.event_type if e.event_type_id else ""
|
||||
|
||||
out.append(
|
||||
{
|
||||
"id": f"evt-{e.id}",
|
||||
"partnerId": f"p{partner.id}",
|
||||
"title": title,
|
||||
"description": e.description or None,
|
||||
"date": _iso_z_from_date(e.start_date),
|
||||
"time": _format_event_time(e.start_time),
|
||||
"venue": venue,
|
||||
"category": category,
|
||||
"ticketPrice": ticket_price,
|
||||
"totalTickets": int(total_tickets) if total_tickets else 0,
|
||||
"ticketsSold": int(sold),
|
||||
"revenue": float(revenue),
|
||||
"status": e.event_status.upper() if e.event_status else None,
|
||||
"submittedAt": None,
|
||||
"createdAt": _iso_z_from_date(e.created_date),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerCreateAPI(APIView):
|
||||
"""
|
||||
@@ -166,6 +436,63 @@ class PartnerListAPI(APIView):
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerDetailWithEventsAPI(APIView):
|
||||
"""
|
||||
Get full partner detail for UI: partner, kycDocuments, dealTerms, ledger, partnerEvents.
|
||||
Body: token, username, partner_id (required).
|
||||
dealTerms and ledger are placeholders ([]) until dedicated models exist.
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
user, token, data, error_response = validate_token_and_get_user(request)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
partner_id = data.get("partner_id")
|
||||
if not partner_id:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "partner_id is required."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
partner = Partner.objects.get(id=partner_id)
|
||||
except Partner.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"status": "error", "message": "Partner not found."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
events = list(
|
||||
Event.objects.filter(partner=partner)
|
||||
.select_related("event_type")
|
||||
.order_by("-created_date")
|
||||
)
|
||||
|
||||
payload = {
|
||||
"status": "success",
|
||||
"partner": _build_partner_detail_payload(partner, request),
|
||||
"kycDocuments": _build_kyc_documents(partner, request),
|
||||
"dealTerms": [],
|
||||
"ledger": [],
|
||||
"partnerEvents": _build_partner_events_payload(partner, events, request),
|
||||
}
|
||||
|
||||
log(
|
||||
"info",
|
||||
"Partner detail with events",
|
||||
request=request,
|
||||
user=user,
|
||||
logger_data={"partner_id": partner.id},
|
||||
)
|
||||
return JsonResponse(payload, status=200)
|
||||
except Exception as e:
|
||||
log("error", "Partner detail with events exception", request=request, logger_data={"error": str(e)})
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class PartnerUpdateAPI(APIView):
|
||||
"""
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.urls import path
|
||||
from partner.api import (
|
||||
PartnerCreateAPI,
|
||||
PartnerListAPI,
|
||||
PartnerDetailWithEventsAPI,
|
||||
PartnerUpdateAPI,
|
||||
PartnerDeleteAPI,
|
||||
PartnerUpdateKYCDocumentsAPI,
|
||||
@@ -22,6 +23,7 @@ from partner.api import (
|
||||
urlpatterns = [
|
||||
path("create/", PartnerCreateAPI.as_view(), name="partner_create"),
|
||||
path("list/", PartnerListAPI.as_view(), name="partner_list"),
|
||||
path("detail-with-events/", PartnerDetailWithEventsAPI.as_view(), name="partner_detail_with_events"),
|
||||
path("update/", PartnerUpdateAPI.as_view(), name="partner_update"),
|
||||
path("delete/", PartnerDeleteAPI.as_view(), name="partner_delete"),
|
||||
path("update-kyc-documents/", PartnerUpdateKYCDocumentsAPI.as_view(), name="partner_update_kyc_documents"),
|
||||
|
||||
Reference in New Issue
Block a user