The changes for the new
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ db.sqlite3
|
|||||||
/media/
|
/media/
|
||||||
/staticfiles/
|
/staticfiles/
|
||||||
.env
|
.env
|
||||||
|
venv/
|
||||||
|
|||||||
18
accounts/migrations/0010_alter_user_id.py
Normal file
18
accounts/migrations/0010_alter_user_id.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-14 19:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0009_user_partner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-14 19:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0008_event_is_partner_event_event_partner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventimages',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
master_data/migrations/0003_alter_eventtype_id.py
Normal file
18
master_data/migrations/0003_alter_eventtype_id.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-14 19:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('master_data', '0002_eventtype_event_type_icon'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventtype',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
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.http import JsonResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models as django_models
|
from django.db import models as django_models
|
||||||
|
from django.db.models import Sum, F, Max, Min, DecimalField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta, datetime, date
|
||||||
|
|
||||||
from rest_framework.views import APIView
|
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 mobile_api.utils import validate_token_and_get_user
|
||||||
from eventify_logger.services import log
|
from eventify_logger.services import log
|
||||||
|
|
||||||
@@ -55,6 +66,265 @@ def _partner_to_dict(partner, request=None):
|
|||||||
return data
|
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")
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
class PartnerCreateAPI(APIView):
|
class PartnerCreateAPI(APIView):
|
||||||
"""
|
"""
|
||||||
@@ -166,6 +436,63 @@ class PartnerListAPI(APIView):
|
|||||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
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")
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
class PartnerUpdateAPI(APIView):
|
class PartnerUpdateAPI(APIView):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.urls import path
|
|||||||
from partner.api import (
|
from partner.api import (
|
||||||
PartnerCreateAPI,
|
PartnerCreateAPI,
|
||||||
PartnerListAPI,
|
PartnerListAPI,
|
||||||
|
PartnerDetailWithEventsAPI,
|
||||||
PartnerUpdateAPI,
|
PartnerUpdateAPI,
|
||||||
PartnerDeleteAPI,
|
PartnerDeleteAPI,
|
||||||
PartnerUpdateKYCDocumentsAPI,
|
PartnerUpdateKYCDocumentsAPI,
|
||||||
@@ -22,6 +23,7 @@ from partner.api import (
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("create/", PartnerCreateAPI.as_view(), name="partner_create"),
|
path("create/", PartnerCreateAPI.as_view(), name="partner_create"),
|
||||||
path("list/", PartnerListAPI.as_view(), name="partner_list"),
|
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("update/", PartnerUpdateAPI.as_view(), name="partner_update"),
|
||||||
path("delete/", PartnerDeleteAPI.as_view(), name="partner_delete"),
|
path("delete/", PartnerDeleteAPI.as_view(), name="partner_delete"),
|
||||||
path("update-kyc-documents/", PartnerUpdateKYCDocumentsAPI.as_view(), name="partner_update_kyc_documents"),
|
path("update-kyc-documents/", PartnerUpdateKYCDocumentsAPI.as_view(), name="partner_update_kyc_documents"),
|
||||||
|
|||||||
Reference in New Issue
Block a user