From b54439a4c2b8b72b1d1c6ba04beddb19c30b237b Mon Sep 17 00:00:00 2001 From: Vivek P Prakash Date: Tue, 24 Mar 2026 19:21:25 +0530 Subject: [PATCH] The changes for the new --- .gitignore | 1 + accounts/migrations/0010_alter_user_id.py | 18 + ...009_alter_event_id_alter_eventimages_id.py | 23 ++ .../migrations/0003_alter_eventtype_id.py | 18 + partner/api.py | 331 +++++++++++++++++- partner/urls.py | 2 + 6 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 accounts/migrations/0010_alter_user_id.py create mode 100644 events/migrations/0009_alter_event_id_alter_eventimages_id.py create mode 100644 master_data/migrations/0003_alter_eventtype_id.py diff --git a/.gitignore b/.gitignore index a795ed0..9578d3b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ db.sqlite3 /media/ /staticfiles/ .env +venv/ diff --git a/accounts/migrations/0010_alter_user_id.py b/accounts/migrations/0010_alter_user_id.py new file mode 100644 index 0000000..c3a02a1 --- /dev/null +++ b/accounts/migrations/0010_alter_user_id.py @@ -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'), + ), + ] diff --git a/events/migrations/0009_alter_event_id_alter_eventimages_id.py b/events/migrations/0009_alter_event_id_alter_eventimages_id.py new file mode 100644 index 0000000..c1db954 --- /dev/null +++ b/events/migrations/0009_alter_event_id_alter_eventimages_id.py @@ -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'), + ), + ] diff --git a/master_data/migrations/0003_alter_eventtype_id.py b/master_data/migrations/0003_alter_eventtype_id.py new file mode 100644 index 0000000..70823af --- /dev/null +++ b/master_data/migrations/0003_alter_eventtype_id.py @@ -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'), + ), + ] diff --git a/partner/api.py b/partner/api.py index 3bde5c8..5355aa6 100644 --- a/partner/api.py +++ b/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): """ diff --git a/partner/urls.py b/partner/urls.py index b0092f8..7cfeb9e 100644 --- a/partner/urls.py +++ b/partner/urls.py @@ -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"),