Files
eventify_backend/partner/api.py

1446 lines
55 KiB
Python
Raw Normal View History

2026-03-24 19:21:25 +05:30
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
2026-03-24 19:21:25 +05:30
from django.db.models import Sum, F, Max, Min, DecimalField
from django.utils import timezone
2026-03-24 19:21:25 +05:30
from datetime import timedelta, datetime, date
from rest_framework.views import APIView
2026-03-24 19:21:25 +05:30
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
User = get_user_model()
def _partner_to_dict(partner, request=None):
"""Serialize Partner for JSON."""
data = model_to_dict(
partner,
fields=[
"id",
"name",
"partner_type",
"primary_contact_person_name",
"primary_contact_person_email",
"primary_contact_person_phone",
"status",
"address",
"city",
"state",
"country",
"website_url",
"pincode",
"latitude",
"longitude",
"is_kyc_compliant",
"kyc_compliance_status",
"kyc_compliance_reason",
"kyc_compliance_document_type",
"kyc_compliance_document_other_type",
"kyc_compliance_document_number",
],
)
# Add document file URL if exists
if partner.kyc_compliance_document_file:
if request:
data["kyc_compliance_document_file"] = request.build_absolute_uri(partner.kyc_compliance_document_file.url)
else:
data["kyc_compliance_document_file"] = partner.kyc_compliance_document_file.url
else:
data["kyc_compliance_document_file"] = None
return data
2026-03-24 19:21:25 +05:30
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):
"""
Create a new Partner.
Body: token, username, name, partner_type, primary_contact_person_name,
primary_contact_person_email, primary_contact_person_phone (required);
address, city, state, country, website_url, pincode, latitude, longitude,
is_kyc_compliant, kyc_compliance_status, kyc_compliance_reason,
kyc_compliance_document_type, kyc_compliance_document_other_type,
kyc_compliance_document_number (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
name = data.get("name")
partner_type = data.get("partner_type")
primary_contact_person_name = data.get("primary_contact_person_name")
primary_contact_person_email = data.get("primary_contact_person_email")
primary_contact_person_phone = data.get("primary_contact_person_phone")
if not all([name, partner_type, primary_contact_person_name, primary_contact_person_email, primary_contact_person_phone]):
return JsonResponse(
{
"status": "error",
"message": "name, partner_type, primary_contact_person_name, primary_contact_person_email, and primary_contact_person_phone are required.",
},
status=400,
)
# Validate partner_type
valid_partner_types = [choice[1] for choice in Partner._meta.get_field("partner_type").choices]
print(valid_partner_types)
if partner_type not in valid_partner_types:
return JsonResponse(
{
"status": "error",
"message": f"Invalid partner_type. Must be one of: {', '.join(valid_partner_types)}",
},
status=400,
)
partner = Partner.objects.create(
name=name,
partner_type=partner_type,
primary_contact_person_name=primary_contact_person_name,
primary_contact_person_email=primary_contact_person_email,
primary_contact_person_phone=primary_contact_person_phone,
address=data.get("address"),
city=data.get("city"),
state=data.get("state"),
country=data.get("country"),
website_url=data.get("website_url"),
pincode=data.get("pincode"),
latitude=data.get("latitude"),
longitude=data.get("longitude"),
is_kyc_compliant=data.get("is_kyc_compliant", False),
kyc_compliance_status=data.get("kyc_compliance_status", "pending"),
kyc_compliance_reason=data.get("kyc_compliance_reason"),
kyc_compliance_document_type=data.get("kyc_compliance_document_type"),
kyc_compliance_document_other_type=data.get("kyc_compliance_document_other_type"),
kyc_compliance_document_number=data.get("kyc_compliance_document_number"),
)
# Handle file upload if provided
if "kyc_compliance_document_file" in request.FILES:
partner.kyc_compliance_document_file = request.FILES["kyc_compliance_document_file"]
partner.save()
log("info", "Partner created", request=request, user=user, logger_data={"partner_id": partner.id, "partner_name": name})
return JsonResponse(
{"status": "success", "partner": _partner_to_dict(partner, request)},
status=201,
)
except Exception as e:
log("error", "Partner create exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerListAPI(APIView):
"""
List Partners, optionally filtered by partner_type or kyc_compliance_status.
Body: token, username, partner_type (optional), kyc_compliance_status (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
qs = Partner.objects.all().order_by("-id")
partner_type = data.get("partner_type")
if partner_type:
qs = qs.filter(partner_type=partner_type)
kyc_compliance_status = data.get("kyc_compliance_status")
if kyc_compliance_status:
qs = qs.filter(kyc_compliance_status=kyc_compliance_status)
partners = [_partner_to_dict(p, request) for p in qs]
return JsonResponse({"status": "success", "partners": partners}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
2026-03-24 19:21:25 +05:30
@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):
"""
Update an existing Partner.
Body: token, username, partner_id (required);
name, partner_type, primary_contact_person_name, primary_contact_person_email,
primary_contact_person_phone, address, city, state, country, website_url,
pincode, latitude, longitude, is_kyc_compliant, kyc_compliance_status,
kyc_compliance_reason, kyc_compliance_document_type,
kyc_compliance_document_other_type, kyc_compliance_document_number (optional).
"""
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,
)
# Update fields if provided
if data.get("name") is not None:
partner.name = data["name"]
if data.get("partner_type") is not None:
valid_partner_types = [choice[0] for choice in Partner._meta.get_field("partner_type").choices]
if data["partner_type"] not in valid_partner_types:
return JsonResponse(
{
"status": "error",
"message": f"Invalid partner_type. Must be one of: {', '.join(valid_partner_types)}",
},
status=400,
)
partner.partner_type = data["partner_type"]
if data.get("primary_contact_person_name") is not None:
partner.primary_contact_person_name = data["primary_contact_person_name"]
if data.get("primary_contact_person_email") is not None:
partner.primary_contact_person_email = data["primary_contact_person_email"]
if data.get("primary_contact_person_phone") is not None:
partner.primary_contact_person_phone = data["primary_contact_person_phone"]
if "address" in data:
partner.address = data["address"]
if "city" in data:
partner.city = data["city"]
if "state" in data:
partner.state = data["state"]
if "country" in data:
partner.country = data["country"]
if "website_url" in data:
partner.website_url = data["website_url"]
if "pincode" in data:
partner.pincode = data["pincode"]
if data.get("latitude") is not None:
try:
partner.latitude = float(data["latitude"])
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "latitude must be numeric."},
status=400,
)
if data.get("longitude") is not None:
try:
partner.longitude = float(data["longitude"])
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "longitude must be numeric."},
status=400,
)
if data.get("is_kyc_compliant") is not None:
partner.is_kyc_compliant = bool(data["is_kyc_compliant"])
if data.get("kyc_compliance_status") is not None:
valid_statuses = [choice[0] for choice in Partner._meta.get_field("kyc_compliance_status").choices]
if data["kyc_compliance_status"] not in valid_statuses:
return JsonResponse(
{
"status": "error",
"message": f"Invalid kyc_compliance_status. Must be one of: {', '.join(valid_statuses)}",
},
status=400,
)
partner.kyc_compliance_status = data["kyc_compliance_status"]
if "kyc_compliance_reason" in data:
partner.kyc_compliance_reason = data["kyc_compliance_reason"]
if "kyc_compliance_document_type" in data:
partner.kyc_compliance_document_type = data["kyc_compliance_document_type"]
if "kyc_compliance_document_other_type" in data:
partner.kyc_compliance_document_other_type = data["kyc_compliance_document_other_type"]
if "kyc_compliance_document_number" in data:
partner.kyc_compliance_document_number = data["kyc_compliance_document_number"]
# Handle file upload if provided
if "kyc_compliance_document_file" in request.FILES:
partner.kyc_compliance_document_file = request.FILES["kyc_compliance_document_file"]
partner.save()
log("info", "Partner updated", request=request, user=user, logger_data={"partner_id": partner_id})
return JsonResponse(
{"status": "success", "partner": _partner_to_dict(partner, request)},
status=200,
)
except Exception as e:
log("error", "Partner update exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerDeleteAPI(APIView):
"""
Delete an existing Partner.
Body: token, username, partner_id.
"""
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,
)
partner_name = partner.name
partner.delete()
log("info", "Partner deleted", request=request, user=user, logger_data={"partner_id": partner_id, "partner_name": partner_name})
return JsonResponse(
{"status": "success", "message": "Partner deleted successfully."},
status=200,
)
except Exception as e:
log("error", "Partner delete exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerUpdateKYCDocumentsAPI(APIView):
"""
Update KYC documents for an existing Partner.
Body: token, username, partner_id (required);
kyc_compliance_document_type, kyc_compliance_document_other_type,
kyc_compliance_document_number (optional);
kyc_compliance_document_file (file upload, optional);
is_kyc_compliant, kyc_compliance_status, kyc_compliance_reason (optional).
"""
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,
)
# Update KYC document fields
if "kyc_compliance_document_type" in data:
document_type = data["kyc_compliance_document_type"]
if document_type: # Allow empty string to clear
valid_types = [choice[0] for choice in Partner._meta.get_field("kyc_compliance_document_type").choices]
if document_type not in valid_types:
return JsonResponse(
{
"status": "error",
"message": f"Invalid kyc_compliance_document_type. Must be one of: {', '.join(valid_types)}",
},
status=400,
)
partner.kyc_compliance_document_type = document_type if document_type else None
if "kyc_compliance_document_other_type" in data:
partner.kyc_compliance_document_other_type = data["kyc_compliance_document_other_type"] or None
if "kyc_compliance_document_number" in data:
partner.kyc_compliance_document_number = data["kyc_compliance_document_number"] or None
# Handle file upload
if "kyc_compliance_document_file" in request.FILES:
partner.kyc_compliance_document_file = request.FILES["kyc_compliance_document_file"]
# Optionally update compliance status fields
if data.get("is_kyc_compliant") is not None:
partner.is_kyc_compliant = bool(data["is_kyc_compliant"])
if data.get("kyc_compliance_status") is not None:
valid_statuses = [choice[0] for choice in Partner._meta.get_field("kyc_compliance_status").choices]
if data["kyc_compliance_status"] not in valid_statuses:
return JsonResponse(
{
"status": "error",
"message": f"Invalid kyc_compliance_status. Must be one of: {', '.join(valid_statuses)}",
},
status=400,
)
partner.kyc_compliance_status = data["kyc_compliance_status"]
if "kyc_compliance_reason" in data:
partner.kyc_compliance_reason = data["kyc_compliance_reason"] or None
partner.save()
return JsonResponse(
{
"status": "success",
"message": "KYC documents updated successfully.",
"partner": _partner_to_dict(partner, request),
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerUpdateAddressLocationAPI(APIView):
"""
Update address and location fields for an existing Partner.
Body: token, username, partner_id (required);
address, city, state, country, website_url, pincode, latitude, longitude (optional).
"""
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,
)
# Update address fields
if "address" in data:
partner.address = data["address"] or None
if "city" in data:
partner.city = data["city"] or None
if "state" in data:
partner.state = data["state"] or None
if "country" in data:
partner.country = data["country"] or None
if "website_url" in data:
partner.website_url = data["website_url"] or None
if "pincode" in data:
partner.pincode = data["pincode"] or None
# Update location coordinates with validation
if data.get("latitude") is not None:
try:
latitude = float(data["latitude"])
# Validate latitude range (-90 to 90)
if latitude < -90 or latitude > 90:
return JsonResponse(
{"status": "error", "message": "latitude must be between -90 and 90."},
status=400,
)
partner.latitude = latitude
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "latitude must be numeric."},
status=400,
)
if data.get("longitude") is not None:
try:
longitude = float(data["longitude"])
# Validate longitude range (-180 to 180)
if longitude < -180 or longitude > 180:
return JsonResponse(
{"status": "error", "message": "longitude must be between -180 and 180."},
status=400,
)
partner.longitude = longitude
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "longitude must be numeric."},
status=400,
)
partner.save()
return JsonResponse(
{
"status": "success",
"message": "Address and location updated successfully.",
"partner": _partner_to_dict(partner, request),
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
def _user_to_dict(user, request=None):
"""Serialize User for JSON."""
data = model_to_dict(
user,
fields=[
"id",
"username",
"email",
"phone_number",
"role",
"is_staff",
"is_customer",
"is_user",
"pincode",
"district",
"state",
"country",
"place",
"latitude",
"longitude",
],
)
# Add profile picture URL if exists
if user.profile_picture:
if request:
data["profile_picture"] = request.build_absolute_uri(user.profile_picture.url)
else:
data["profile_picture"] = user.profile_picture.url
else:
data["profile_picture"] = None
return data
@method_decorator(csrf_exempt, name="dispatch")
class PartnerCreateUserAPI(APIView):
"""
Create a user with partner-related role (partner, partner_manager, partner_staff, partner_customer).
Body: token, username, email, password, role (required);
phone_number, partner_id, pincode, district, state, country, place, latitude, longitude (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
username = data.get("username")
email = data.get("email")
password = data.get("password")
role = data.get("role")
partner_id = data.get("partner_id")
if not all([username, email, password, role]):
return JsonResponse(
{
"status": "error",
"message": "username, email, password, and role are required.",
},
status=400,
)
# Validate role - must be one of the partner-related roles
valid_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
if role not in valid_partner_roles:
return JsonResponse(
{
"status": "error",
"message": f"Invalid role. Must be one of: {', '.join(valid_partner_roles)}",
},
status=400,
)
# Check if username already exists
if User.objects.filter(username=username).exists():
return JsonResponse(
{"status": "error", "message": "Username already exists."},
status=400,
)
# Check if email already exists
if User.objects.filter(email=email).exists():
return JsonResponse(
{"status": "error", "message": "Email already exists."},
status=400,
)
# Validate partner_id if provided
partner = None
if partner_id:
try:
partner = Partner.objects.get(id=partner_id)
except Partner.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "Partner not found."},
status=404,
)
# Create user
new_user = User.objects.create_user(
username=username,
email=email,
password=password,
role=role,
phone_number=data.get("phone_number"),
pincode=data.get("pincode"),
district=data.get("district"),
state=data.get("state"),
country=data.get("country"),
place=data.get("place"),
)
# Set location coordinates if provided
if data.get("latitude") is not None:
try:
latitude = float(data["latitude"])
if latitude < -90 or latitude > 90:
return JsonResponse(
{"status": "error", "message": "latitude must be between -90 and 90."},
status=400,
)
new_user.latitude = latitude
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "latitude must be numeric."},
status=400,
)
if data.get("longitude") is not None:
try:
longitude = float(data["longitude"])
if longitude < -180 or longitude > 180:
return JsonResponse(
{"status": "error", "message": "longitude must be between -180 and 180."},
status=400,
)
new_user.longitude = longitude
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "longitude must be numeric."},
status=400,
)
# Handle profile picture upload if provided
if "profile_picture" in request.FILES:
new_user.profile_picture = request.FILES["profile_picture"]
new_user.save()
response_data = {
"status": "success",
"message": f"User created successfully with role: {role}.",
"user": _user_to_dict(new_user, request),
}
# Include partner info if linked
if partner:
response_data["partner"] = {
"id": partner.id,
"name": partner.name,
"partner_type": partner.partner_type,
}
log("info", "Partner user created", request=request, user=user, logger_data={"new_user_id": new_user.id, "username": new_user.username, "role": role})
return JsonResponse(response_data, status=201)
except Exception as e:
log("error", "Partner create user exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerListUsersAPI(APIView):
"""
List users associated with a partner (users with partner-related roles).
Body: token, username, partner_id (required);
role (optional filter: partner, partner_manager, partner_staff, partner_customer).
"""
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,
)
# Validate partner exists
try:
partner = Partner.objects.get(id=partner_id)
except Partner.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "Partner not found."},
status=404,
)
# Filter users by partner-related roles
partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
qs = User.objects.filter(role__in=partner_roles).order_by("-id")
# Optional role filter
role_filter = data.get("role")
if role_filter:
if role_filter not in partner_roles:
return JsonResponse(
{
"status": "error",
"message": f"Invalid role filter. Must be one of: {', '.join(partner_roles)}",
},
status=400,
)
qs = qs.filter(role=role_filter)
# Optionally filter by matching contact email or phone (if partner contact info matches user)
# This is a heuristic approach since there's no direct FK relationship
# You can enhance this logic based on your business requirements
match_by_contact = data.get("match_by_contact", False)
if match_by_contact:
qs = qs.filter(
django_models.Q(email=partner.primary_contact_person_email)
| django_models.Q(phone_number=partner.primary_contact_person_phone)
)
users = [_user_to_dict(u, request) for u in qs]
response_data = {
"status": "success",
"partner": {
"id": partner.id,
"name": partner.name,
"partner_type": partner.partner_type,
},
"users": users,
"total_count": len(users),
}
return JsonResponse(response_data, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerUpdateUserAPI(APIView):
"""
Update a partner user (user with partner-related role).
Body: token, username, user_id (required);
email, phone_number, role (must be partner-related), pincode, district, state,
country, place, latitude, longitude, profile_picture (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
user_id = data.get("user_id")
if not user_id:
return JsonResponse(
{"status": "error", "message": "user_id is required."},
status=400,
)
try:
target_user = User.objects.get(id=user_id)
except User.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "User not found."},
status=404,
)
# Validate that the user has a partner-related role
partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
if target_user.role not in partner_roles:
return JsonResponse(
{
"status": "error",
"message": "User is not a partner-related user. Only users with partner roles can be updated.",
},
status=400,
)
# Update fields if provided
if data.get("email") is not None:
new_email = data["email"]
# Check if email already exists for another user
if User.objects.filter(email=new_email).exclude(id=user_id).exists():
return JsonResponse(
{"status": "error", "message": "Email already exists for another user."},
status=400,
)
target_user.email = new_email
if data.get("phone_number") is not None:
target_user.phone_number = data["phone_number"] or None
if data.get("role") is not None:
new_role = data["role"]
if new_role not in partner_roles:
return JsonResponse(
{
"status": "error",
"message": f"Invalid role. Must be one of: {', '.join(partner_roles)}",
},
status=400,
)
target_user.role = new_role
if "pincode" in data:
target_user.pincode = data["pincode"] or None
if "district" in data:
target_user.district = data["district"] or None
if "state" in data:
target_user.state = data["state"] or None
if "country" in data:
target_user.country = data["country"] or None
if "place" in data:
target_user.place = data["place"] or None
if data.get("latitude") is not None:
try:
latitude = float(data["latitude"])
if latitude < -90 or latitude > 90:
return JsonResponse(
{"status": "error", "message": "latitude must be between -90 and 90."},
status=400,
)
target_user.latitude = latitude
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "latitude must be numeric."},
status=400,
)
if data.get("longitude") is not None:
try:
longitude = float(data["longitude"])
if longitude < -180 or longitude > 180:
return JsonResponse(
{"status": "error", "message": "longitude must be between -180 and 180."},
status=400,
)
target_user.longitude = longitude
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "longitude must be numeric."},
status=400,
)
# Handle profile picture upload if provided
if "profile_picture" in request.FILES:
target_user.profile_picture = request.FILES["profile_picture"]
# Handle password update if provided
if data.get("password"):
target_user.set_password(data["password"])
target_user.save()
log("info", "Partner user updated", request=request, user=user, logger_data={"user_id": user_id})
return JsonResponse(
{
"status": "success",
"message": "Partner user updated successfully.",
"user": _user_to_dict(target_user, request),
},
status=200,
)
except Exception as e:
log("error", "Partner update user exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerDeleteUserAPI(APIView):
"""
Delete a partner user (user with partner-related role).
Body: token, username, user_id (required).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
user_id = data.get("user_id")
if not user_id:
return JsonResponse(
{"status": "error", "message": "user_id is required."},
status=400,
)
try:
target_user = User.objects.get(id=user_id)
except User.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "User not found."},
status=404,
)
# Validate that the user has a partner-related role
partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
if target_user.role not in partner_roles:
return JsonResponse(
{
"status": "error",
"message": "User is not a partner-related user. Only users with partner roles can be deleted.",
},
status=400,
)
# Prevent deleting yourself
if target_user.id == user.id:
return JsonResponse(
{"status": "error", "message": "You cannot delete your own account."},
status=400,
)
username = target_user.username
target_user.delete()
log("info", "Partner user deleted", request=request, user=user, logger_data={"user_id": user_id, "username": username})
return JsonResponse(
{
"status": "success",
"message": f"Partner user '{username}' deleted successfully.",
},
status=200,
)
except Exception as e:
log("error", "Partner delete user exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)
# ============================================================================
# Partner List APIs (Filtered by Criteria)
# ============================================================================
@method_decorator(csrf_exempt, name="dispatch")
class PartnerListAllAPI(APIView):
"""
List All Partners API.
Body: token, username (required).
Returns: list of all partners.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
if error_response:
return error_response
partners = Partner.objects.all().order_by("-id")
partners_list = [_partner_to_dict(p, request) for p in partners]
return JsonResponse({
"status": "success",
"partners": partners_list,
"total_count": len(partners_list)
}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerListKYCCompliantAPI(APIView):
"""
List KYC Compliant Partners API.
Body: token, username (required).
Returns: list of partners where is_kyc_compliant=True and kyc_compliance_status='approved'.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
if error_response:
return error_response
partners = Partner.objects.filter(
is_kyc_compliant=True,
kyc_compliance_status='approved'
).order_by("-id")
partners_list = [_partner_to_dict(p, request) for p in partners]
return JsonResponse({
"status": "success",
"partners": partners_list,
"total_count": len(partners_list)
}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerListKYCPendingAPI(APIView):
"""
List KYC Pending Partners API.
Body: token, username (required).
Returns: list of partners where kyc_compliance_status='pending'.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
if error_response:
return error_response
partners = Partner.objects.filter(
kyc_compliance_status='pending'
).order_by("-id")
partners_list = [_partner_to_dict(p, request) for p in partners]
return JsonResponse({
"status": "success",
"partners": partners_list,
"total_count": len(partners_list)
}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerListHighRiskAPI(APIView):
"""
List High Risk Partners API.
Body: token, username (required).
Returns: list of partners where kyc_compliance_status='high_risk'.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
if error_response:
return error_response
partners = Partner.objects.filter(
kyc_compliance_status='high_risk'
).order_by("-id")
partners_list = [_partner_to_dict(p, request) for p in partners]
return JsonResponse({
"status": "success",
"partners": partners_list,
"total_count": len(partners_list)
}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerListJoinedThisWeekAPI(APIView):
"""
List Partners Joined This Week API.
Body: token, username (required).
Returns: list of partners created in the last 7 days.
Note: This API uses id-based filtering as a proxy for creation date.
For accurate results, consider adding a created_date field to the Partner model.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request, error_status_code=True)
if error_response:
return error_response
# Calculate date range for this week
today = timezone.now().date()
week_start = today - timedelta(days=7)
# Since Partner model doesn't have created_date, we'll use id as a proxy
# Get the highest id to approximate recent partners
# This is a workaround - ideally Partner model should have created_date field
latest_partner = Partner.objects.order_by('-id').first()
if latest_partner:
# Get partners created in the last week based on id approximation
# This assumes sequential id assignment
# For accurate results, add created_date field to Partner model
partners = Partner.objects.filter(
id__gte=latest_partner.id - 1000 # Approximate filter
).order_by("-id")
# Alternative: If you want to add created_date field, use:
# partners = Partner.objects.filter(
# created_date__gte=week_start
# ).order_by("-created_date")
else:
partners = Partner.objects.none()
partners_list = [_partner_to_dict(p, request) for p in partners]
return JsonResponse({
"status": "success",
"partners": partners_list,
"total_count": len(partners_list),
"note": "This uses id-based approximation. Consider adding created_date field for accurate results."
}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)