The new updates of partners and user

Made-with: Cursor
This commit is contained in:
Vivek P Prakash
2026-03-15 00:29:17 +05:30
parent 88b3aafb0b
commit c04395afc9
65 changed files with 5242 additions and 341 deletions

Submodule .claude/worktrees/strange-ellis added at 88b3aafb0b

946
accounts/api.py Normal file
View File

@@ -0,0 +1,946 @@
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 authenticate, logout
from rest_framework.views import APIView
from rest_framework.authtoken.models import Token
import json
from .models import User
from mobile_api.utils import validate_token_and_get_user
def _partner_user_to_dict(user, request=None):
"""Serialize partner-related User for JSON (same structure as _user_to_dict)."""
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",
"first_name",
"last_name",
],
)
# Add profile picture URL if exists
if getattr(user, "profile_picture", None):
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
def _user_to_dict(user, request=None):
"""Serialize any User for JSON (admin/staff or partner)."""
return _partner_user_to_dict(user, request)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerLoginAPI(APIView):
"""
Partner Login API.
Body: username (or email), password (required).
Returns: token, user data.
"""
def post(self, request):
try:
# Parse JSON or form data
is_multipart = request.content_type and "multipart/form-data" in request.content_type
if is_multipart:
data = request.POST.dict()
else:
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse(
{"status": "error", "message": "Invalid JSON"},
status=400,
)
username = data.get("username") or data.get("email")
password = data.get("password")
if not username or not password:
return JsonResponse(
{"status": "error", "message": "username and password are required."},
status=400,
)
# Authenticate user
user = authenticate(request, username=username, password=password)
if not user:
return JsonResponse(
{"status": "error", "message": "Invalid username or password."},
status=401,
)
# Check if user has partner role
partner_roles = ["partner", "partner_manager", "partner_staff"]
if user.role not in partner_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to access partner portal."},
status=403,
)
# Get or create token
token, _ = Token.objects.get_or_create(user=user)
return JsonResponse(
{
"status": "success",
"message": "Login successful",
"token": token.key,
"user": _partner_user_to_dict(user, request),
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerLogoutAPI(APIView):
"""
Partner Logout API.
Body: token, username (required).
Returns: success message.
"""
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
# Check if user has partner role
partner_roles = ["partner", "partner_manager", "partner_staff"]
if user.role not in partner_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to access partner portal."},
status=403,
)
# Delete token
token.delete()
return JsonResponse(
{
"status": "success",
"message": "Logged out successfully.",
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerDashboardAPI(APIView):
"""
Partner Dashboard API.
Body: token, username (required).
Returns: dashboard statistics.
"""
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
# Check if user has partner role
partner_roles = ["partner", "partner_manager", "partner_staff"]
if user.role not in partner_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to access this page."},
status=403,
)
# Get statistics for partner users (including partner_customer)
all_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
partner_users = User.objects.filter(role__in=all_partner_roles)
total_partner_users = partner_users.count()
return JsonResponse(
{
"status": "success",
"dashboard": {
"total_partner_users": total_partner_users,
},
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerListUsersAPI(APIView):
"""
Partner List Users API.
Body: token, username (required);
role (optional filter: partner, partner_manager, partner_staff, partner_customer).
Returns: list of partner users.
"""
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
# Check if user has partner role
partner_roles = ["partner", "partner_manager", "partner_staff"]
if user.role not in partner_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to access this page."},
status=403,
)
# Filter users by partner-related roles
all_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
qs = User.objects.filter(role__in=all_partner_roles).order_by("-id")
# Optional role filter
role_filter = data.get("role")
if role_filter:
if role_filter not in all_partner_roles:
return JsonResponse(
{
"status": "error",
"message": f"Invalid role filter. Must be one of: {', '.join(all_partner_roles)}",
},
status=400,
)
qs = qs.filter(role=role_filter)
users = [_partner_user_to_dict(u, request) for u in qs]
return JsonResponse(
{
"status": "success",
"users": users,
"total_count": len(users),
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerCreateUserAPI(APIView):
"""
Partner Create User API.
Body: token, username, username (for new user), email, password, role (required);
full_name, phone_number, pincode, district, state, country, place, latitude, longitude (optional).
Returns: created user data.
"""
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
# Check if user has partner role
partner_roles = ["partner", "partner_manager", "partner_staff"]
if user.role not in partner_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to access this page."},
status=403,
)
# Extract user data
new_username = data.get("username")
email = data.get("email")
password = data.get("password")
role = data.get("role")
full_name = data.get("full_name", "").strip()
if not all([new_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=new_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,
)
# Create user
new_user = User.objects.create_user(
username=new_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"),
)
# Handle full_name - split into first_name and last_name
if full_name:
parts = full_name.split(None, 1)
new_user.first_name = parts[0]
if len(parts) > 1:
new_user.last_name = parts[1]
# 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()
return JsonResponse(
{
"status": "success",
"message": f"User created successfully with role: {role}.",
"user": _partner_user_to_dict(new_user, request),
},
status=201,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerUpdateUserAPI(APIView):
"""
Partner Update User API.
Body: token, username, user_id (required);
email, phone_number, role, full_name, pincode, district, state,
country, place, latitude, longitude, password, profile_picture (optional).
Returns: updated user data.
"""
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
# Check if user has partner role
partner_roles = ["partner", "partner_manager", "partner_staff"]
if user.role not in partner_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to access this page."},
status=403,
)
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
all_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
if target_user.role not in all_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 all_partner_roles:
return JsonResponse(
{
"status": "error",
"message": f"Invalid role. Must be one of: {', '.join(all_partner_roles)}",
},
status=400,
)
target_user.role = new_role
# Handle full_name
if data.get("full_name"):
full_name = data["full_name"].strip()
if full_name:
parts = full_name.split(None, 1)
target_user.first_name = parts[0]
if len(parts) > 1:
target_user.last_name = parts[1]
else:
target_user.last_name = ""
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()
return JsonResponse(
{
"status": "success",
"message": "Partner user updated successfully.",
"user": _partner_user_to_dict(target_user, request),
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PartnerDeleteUserAPI(APIView):
"""
Partner Delete User API.
Body: token, username, user_id (required).
Returns: success message.
"""
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
# Check if user has partner role
partner_roles = ["partner", "partner_manager", "partner_staff"]
if user.role not in partner_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to access this page."},
status=403,
)
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
all_partner_roles = ["partner", "partner_manager", "partner_staff", "partner_customer"]
if target_user.role not in all_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()
return JsonResponse(
{
"status": "success",
"message": f"Partner user '{username}' deleted successfully.",
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class LoginAPI(APIView):
"""
Admin/Staff Login API (accounts).
Body: username (or email), password (required).
Returns: token and user details for admin/manager/staff roles.
"""
def post(self, request):
try:
# Parse JSON or form data
is_multipart = request.content_type and "multipart/form-data" in request.content_type
if is_multipart:
data = request.POST.dict()
else:
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse(
{"status": "error", "message": "Invalid JSON"},
status=400,
)
username = data.get("username") or data.get("email")
password = data.get("password")
if not username or not password:
return JsonResponse(
{"status": "error", "message": "username and password are required."},
status=400,
)
user = authenticate(request, username=username, password=password)
if not user:
return JsonResponse(
{"status": "error", "message": "Invalid username or password."},
status=401,
)
# Only allow admin/manager/staff to use this login
allowed_roles = ["admin", "manager", "staff"]
if user.role not in allowed_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to access the admin portal."},
status=403,
)
token, _ = Token.objects.get_or_create(user=user)
return JsonResponse(
{
"status": "success",
"message": "Login successful",
"token": token.key,
"user": _user_to_dict(user, request),
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class LogoutAPI(APIView):
"""
Logout API for token-based sessions.
Body: token, username (required).
"""
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
logout(request)
token.delete()
return JsonResponse(
{
"status": "success",
"message": "Logout successful.",
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class UserListAPI(APIView):
"""
List users (admin / manager / staff only).
Body: token, username (required); optional role filter.
"""
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
# Only allow admin/manager/staff to list users
allowed_roles = ["admin", "manager", "staff"]
if user.role not in allowed_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to list users."},
status=403,
)
qs = User.objects.all().order_by("-id")
role_filter = data.get("role")
if role_filter:
qs = qs.filter(role=role_filter)
users = [_user_to_dict(u, request) for u in qs]
return JsonResponse(
{
"status": "success",
"users": users,
"total_count": len(users),
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class UserCreateAPI(APIView):
"""
Create a user (admin / manager / staff only).
Body: token, username, new_username, email, password, role ('admin'|'manager'|'staff'), phone_number (optional).
"""
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
allowed_roles = ["admin", "manager", "staff"]
if user.role not in allowed_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to create users."},
status=403,
)
new_username = data.get("username") or data.get("new_username")
email = data.get("email")
password = data.get("password")
role = data.get("role")
if not all([new_username, email, password, role]):
return JsonResponse(
{
"status": "error",
"message": "username, email, password, and role are required.",
},
status=400,
)
valid_roles = ["admin", "manager", "staff"]
if role not in valid_roles:
return JsonResponse(
{
"status": "error",
"message": f"Invalid role. Must be one of: {', '.join(valid_roles)}",
},
status=400,
)
if User.objects.filter(username=new_username).exists():
return JsonResponse(
{"status": "error", "message": "Username already exists."},
status=400,
)
if User.objects.filter(email=email).exists():
return JsonResponse(
{"status": "error", "message": "Email already exists."},
status=400,
)
new_user = User.objects.create_user(
username=new_username,
email=email,
password=password,
role=role,
phone_number=data.get("phone_number"),
)
return JsonResponse(
{
"status": "success",
"message": f"User created successfully with role: {role}.",
"user": _user_to_dict(new_user, request),
},
status=201,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class UserUpdateAPI(APIView):
"""
Update a user (admin / manager / staff only).
Body: token, username, user_id (required); email, phone_number, role (optional).
"""
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
allowed_roles = ["admin", "manager", "staff"]
if user.role not in allowed_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to update users."},
status=403,
)
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,
)
if data.get("email") is not None:
new_email = data["email"]
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"]
valid_roles = ["admin", "manager", "staff"]
if new_role not in valid_roles:
return JsonResponse(
{
"status": "error",
"message": f"Invalid role. Must be one of: {', '.join(valid_roles)}",
},
status=400,
)
target_user.role = new_role
target_user.save()
return JsonResponse(
{
"status": "success",
"message": "User updated successfully.",
"user": _user_to_dict(target_user, request),
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class UserDeleteAPI(APIView):
"""
Delete a user (admin / manager / staff only).
Body: token, username, user_id (required).
"""
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
allowed_roles = ["admin", "manager", "staff"]
if user.role not in allowed_roles:
return JsonResponse(
{"status": "error", "message": "You are not authorized to delete users."},
status=403,
)
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,
)
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()
return JsonResponse(
{
"status": "success",
"message": f"User '{username}' deleted successfully.",
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)

View File

@@ -88,3 +88,110 @@ class LoginForm(AuthenticationForm):
"placeholder": "Enter password"
})
)
class PartnerUserForm(forms.ModelForm):
full_name = forms.CharField(
max_length=150,
required=True,
label="Full Name"
)
password = forms.CharField(
widget=forms.PasswordInput,
label="Password",
required=True,
help_text="Required for new users. Leave blank if you don't want to change the password when editing."
)
confirm_password = forms.CharField(
widget=forms.PasswordInput,
label="Confirm Password",
required=True
)
phone_number = forms.CharField(
max_length=15,
required=False,
label="Phone Number"
)
ROLE_CHOICES = [
('partner', 'Partner'),
('partner_manager', 'Partner Manager'),
('partner_staff', 'Partner Staff'),
('partner_customer', 'Partner Customer'),
]
role = forms.ChoiceField(
choices=ROLE_CHOICES,
required=True,
label="Role"
)
class Meta:
model = User
fields = ["username", "email", "phone_number", "role"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
# Make password fields optional for updates, required for new users
if self.instance and self.instance.pk:
self.fields['password'].required = False
self.fields['confirm_password'].required = False
# Pre-populate full_name from first_name and last_name
if self.instance.first_name or self.instance.last_name:
self.fields['full_name'].initial = f"{self.instance.first_name} {self.instance.last_name}".strip()
else:
# For new users, password is required
self.fields['password'].required = True
self.fields['confirm_password'].required = True
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
confirm_password = cleaned_data.get("confirm_password")
# For new users, password is required
if not self.instance or not self.instance.pk:
if not password:
self.add_error("password", "Password is required for new users.")
if not confirm_password:
self.add_error("confirm_password", "Please confirm your password.")
# Validate password match if password is provided
if password or confirm_password:
if password != confirm_password:
self.add_error("confirm_password", "Passwords do not match!")
return cleaned_data
def save(self, commit=True):
user = super().save(commit=False)
# Set password - required for new users, optional for updates
password = self.cleaned_data.get('password')
if password:
user.set_password(password)
elif not user.pk:
# New user must have a password
raise ValueError("Password is required for new users.")
# Save phone_number and role to the User model
user.phone_number = self.cleaned_data.get("phone_number")
user.role = self.cleaned_data.get("role")
# Handle full_name - split into first_name and last_name
full_name = self.cleaned_data.get("full_name", "").strip()
if full_name:
parts = full_name.split(None, 1)
user.first_name = parts[0]
if len(parts) > 1:
user.last_name = parts[1]
else:
user.last_name = ""
if commit:
user.save()
return user

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.27 on 2026-03-13 16:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0007_alter_user_profile_picture'),
]
operations = [
migrations.AlterField(
model_name='user',
name='role',
field=models.CharField(choices=[('admin', 'Admin'), ('manager', 'Manager'), ('staff', 'Staff'), ('customer', 'Customer'), ('partner', 'Partner'), ('partner_manager', 'Partner Manager'), ('partner_staff', 'Partner Staff'), ('partner_customer', 'Partner Customer')], default='Staff', max_length=20),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.27 on 2026-03-14 07:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('partner', '0001_initial'),
('accounts', '0008_alter_user_role'),
]
operations = [
migrations.AddField(
model_name='user',
name='partner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partner.partner'),
),
]

View File

@@ -2,18 +2,24 @@ from django.contrib.auth.models import AbstractUser
from django.db import models
from accounts.manager import UserManager
from partner.models import Partner
ROLE_CHOICES = (
('admin', 'Admin'),
('manager', 'Manager'),
('staff', 'Staff'),
('customer', 'Customer'),
('partner', 'Partner'),
('partner_manager', 'Partner Manager'),
('partner_staff', 'Partner Staff'),
('partner_customer', 'Partner Customer'),
)
class User(AbstractUser):
phone_number = models.CharField(max_length=15, blank=True, null=True)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='Staff')
partner = models.ForeignKey(Partner, on_delete=models.CASCADE, blank=True, null=True)
is_staff = models.BooleanField(default=False)
is_customer = models.BooleanField(default=False)
is_user = models.BooleanField(default=False)

View File

@@ -1,14 +1,37 @@
from django.urls import path
from . import views
from . import views, api
app_name = 'accounts'
app_name = "accounts"
urlpatterns = [
path('login/', views.login_view, name='login'),
path('logout/', views.logout_view, name='logout'),
path('dashboard/', views.dashboard, name='dashboard'),
path('users/', views.UserListView.as_view(), name='user_list'),
path('users/add/', views.UserCreateView.as_view(), name='user_add'),
path('users/<int:pk>/edit/', views.UserUpdateView.as_view(), name='user_edit'),
path('users/<int:pk>/delete/', views.UserDeleteView.as_view(), name='user_delete'),
path("login/", views.login_view, name="login"),
path("logout/", views.logout_view, name="logout"),
path("dashboard/", views.dashboard, name="dashboard"),
path("users/", views.UserListView.as_view(), name="user_list"),
path("users/add/", views.UserCreateView.as_view(), name="user_add"),
path("users/<int:pk>/edit/", views.UserUpdateView.as_view(), name="user_edit"),
path("users/<int:pk>/delete/", views.UserDeleteView.as_view(), name="user_delete"),
]
# Core account APIs (admin/staff)
urlpatterns += [
path("api/login/", api.LoginAPI.as_view(), name="api_login"),
path("api/logout/", api.LogoutAPI.as_view(), name="api_logout"),
path("api/users/list/", api.UserListAPI.as_view(), name="api_user_list"),
path("api/users/create/", api.UserCreateAPI.as_view(), name="api_user_create"),
path("api/users/update/", api.UserUpdateAPI.as_view(), name="api_user_update"),
path("api/users/delete/", api.UserDeleteAPI.as_view(), name="api_user_delete"),
]
# Partner APIs
urlpatterns += [
path("api/partner/login/", api.PartnerLoginAPI.as_view(), name="partner_api_login"),
path("api/partner/logout/", api.PartnerLogoutAPI.as_view(), name="partner_api_logout"),
path("api/partner/dashboard/", api.PartnerDashboardAPI.as_view(), name="partner_api_dashboard"),
path("api/partner/users/list/", api.PartnerListUsersAPI.as_view(), name="partner_api_user_list"),
path("api/partner/users/create/", api.PartnerCreateUserAPI.as_view(), name="partner_api_user_create"),
path("api/partner/users/update/", api.PartnerUpdateUserAPI.as_view(), name="partner_api_user_update"),
path("api/partner/users/delete/", api.PartnerDeleteUserAPI.as_view(), name="partner_api_user_delete"),
]

View File

@@ -1,16 +1,16 @@
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.views import generic
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from .models import User
from .forms import LoginForm
from .forms import UserForm
from .forms import LoginForm, UserForm, PartnerUserForm
from events.models import Event
from master_data.models import EventType
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import redirect
from django.contrib import messages
from eventify_logger.services import log
def dashboard(request):
@@ -62,16 +62,150 @@ def login_view(request):
user = form.get_user()
login(request, user)
if user.role == 'admin' or user.role == 'manager' or user.role == 'staff':
log("info", "Admin/Manager/Staff login", request=request, user=user)
return redirect("accounts:dashboard")
else:
log("warning", "Login attempt - user not authorized", request=request, user=user)
messages.error(request, "You are not authorized to access this page.")
else:
log("warning", "Invalid login attempt", request=request)
messages.error(request, "Invalid username or password")
return render(request, "accounts/login.html", {"form": form})
def logout_view(request):
if request.user.is_authenticated:
log("info", "User logout", request=request, user=request.user)
logout(request)
messages.success(request, "You have been logged out successfully.")
return redirect("accounts:login")
# Partner Views Mixin
class PartnerRequiredMixin(LoginRequiredMixin):
"""Mixin to ensure user has partner role (partner, partner_manager, partner_staff)"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
partner_roles = ['partner', 'partner_manager', 'partner_staff']
if request.user.role not in partner_roles:
raise PermissionDenied("You are not authorized to access this page.")
return super().dispatch(request, *args, **kwargs)
# Partner Login/Logout/Dashboard
def partner_login_view(request):
if request.user.is_authenticated:
partner_roles = ['partner', 'partner_manager', 'partner_staff']
if request.user.role in partner_roles:
return redirect("accounts:partner_dashboard")
else:
messages.error(request, "You are not authorized to access partner portal.")
return redirect("accounts:login")
form = LoginForm(request, data=request.POST or None)
if request.method == "POST":
if form.is_valid():
user = form.get_user()
partner_roles = ['partner', 'partner_manager', 'partner_staff']
if user.role in partner_roles:
log("info", "Partner portal login", request=request, user=user)
login(request, user)
return redirect("accounts:partner_dashboard")
else:
log("warning", "Partner login - user not authorized", request=request, user=user)
messages.error(request, "You are not authorized to access partner portal.")
else:
log("warning", "Partner portal - invalid login attempt", request=request)
messages.error(request, "Invalid username or password")
return render(request, "partner/login.html", {"form": form})
def partner_logout_view(request):
if request.user.is_authenticated:
log("info", "Partner portal logout", request=request, user=request.user)
logout(request)
messages.success(request, "You have been logged out successfully.")
return redirect("accounts:partner_login")
def partner_dashboard(request):
"""Partner dashboard view"""
partner_roles = ['partner', 'partner_manager', 'partner_staff']
if not request.user.is_authenticated or request.user.role not in partner_roles:
messages.error(request, "You are not authorized to access this page.")
return redirect("accounts:partner_login")
# Get statistics for partner users (including partner_customer)
all_partner_roles = ['partner', 'partner_manager', 'partner_staff', 'partner_customer']
partner_users = User.objects.filter(role__in=all_partner_roles)
total_partner_users = partner_users.count()
# You can add more partner-specific statistics here
# For example, events created by partner, bookings, etc.
return render(request, 'partner/dashboard.html', {
'total_partner_users': total_partner_users,
})
# Partner User Management Views
class PartnerUserListView(PartnerRequiredMixin, generic.ListView):
model = User
template_name = 'partner/user_list.html'
context_object_name = 'users'
paginate_by = 20
def get_queryset(self):
"""Filter users to show only partner-related roles"""
partner_roles = ['partner', 'partner_manager', 'partner_staff', 'partner_customer']
return User.objects.filter(role__in=partner_roles).order_by('-id')
class PartnerUserCreateView(PartnerRequiredMixin, generic.CreateView):
model = User
form_class = PartnerUserForm
template_name = 'partner/user_form.html'
success_url = reverse_lazy('accounts:partner_user_list')
def form_valid(self, form):
messages.success(self.request, "Partner user created successfully.")
return super().form_valid(form)
class PartnerUserUpdateView(PartnerRequiredMixin, generic.UpdateView):
model = User
form_class = PartnerUserForm
template_name = 'partner/user_form.html'
success_url = reverse_lazy('accounts:partner_user_list')
def get_queryset(self):
"""Only allow editing users with partner-related roles"""
partner_roles = ['partner', 'partner_manager', 'partner_staff', 'partner_customer']
return User.objects.filter(role__in=partner_roles)
def form_valid(self, form):
messages.success(self.request, "Partner user updated successfully.")
return super().form_valid(form)
class PartnerUserDeleteView(PartnerRequiredMixin, generic.DeleteView):
model = User
template_name = 'partner/user_confirm_delete.html'
success_url = reverse_lazy('accounts:partner_user_list')
def get_queryset(self):
"""Only allow deleting users with partner-related roles"""
partner_roles = ['partner', 'partner_manager', 'partner_staff', 'partner_customer']
return User.objects.filter(role__in=partner_roles)
def delete(self, request, *args, **kwargs):
# Prevent users from deleting themselves
if self.get_object().id == request.user.id:
messages.error(request, "You cannot delete your own account.")
return redirect(self.success_url)
messages.success(request, "Partner user deleted successfully.")
return super().delete(request, *args, **kwargs)

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

432
banking_operations/api.py Normal file
View File

@@ -0,0 +1,432 @@
import uuid
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 rest_framework.views import APIView
from banking_operations.models import PaymentGateway, PaymentGatewayCredentials
from mobile_api.utils import validate_token_and_get_user
def _payment_gateway_to_dict(gateway, request=None):
"""Serialize PaymentGateway for JSON."""
data = model_to_dict(
gateway,
fields=[
"id",
"payment_gateway_id",
"payment_gateway_name",
"payment_gateway_description",
"payment_gateway_url",
"payment_gateway_api_key",
"payment_gateway_api_secret",
"payment_gateway_api_url",
"payment_gateway_api_version",
"payment_gateway_api_method",
"is_active",
"created_date",
"updated_date",
"gateway_priority",
],
)
# Add logo URL if exists
if gateway.payment_gateway_logo:
if request:
data["payment_gateway_logo"] = request.build_absolute_uri(gateway.payment_gateway_logo.url)
else:
data["payment_gateway_logo"] = gateway.payment_gateway_logo.url
else:
data["payment_gateway_logo"] = None
return data
@method_decorator(csrf_exempt, name="dispatch")
class PaymentGatewayCreateAPI(APIView):
"""
Create a new PaymentGateway.
Body: token, username, payment_gateway_name, payment_gateway_description,
payment_gateway_api_key, payment_gateway_api_secret, payment_gateway_api_version,
payment_gateway_api_method (required);
payment_gateway_logo (file upload), payment_gateway_url, payment_gateway_api_url,
is_active, gateway_priority (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
payment_gateway_name = data.get("payment_gateway_name")
payment_gateway_description = data.get("payment_gateway_description")
payment_gateway_api_key = data.get("payment_gateway_api_key")
payment_gateway_api_secret = data.get("payment_gateway_api_secret")
payment_gateway_api_version = data.get("payment_gateway_api_version")
payment_gateway_api_method = data.get("payment_gateway_api_method")
if not all([
payment_gateway_name,
payment_gateway_description,
payment_gateway_api_key,
payment_gateway_api_secret,
payment_gateway_api_version,
payment_gateway_api_method,
]):
return JsonResponse(
{
"status": "error",
"message": "payment_gateway_name, payment_gateway_description, payment_gateway_api_key, "
"payment_gateway_api_secret, payment_gateway_api_version, and "
"payment_gateway_api_method are required.",
},
status=400,
)
# Generate payment_gateway_id if not provided
payment_gateway_id = data.get("payment_gateway_id")
if not payment_gateway_id:
payment_gateway_id = str(uuid.uuid4().hex[:10]).upper()
gateway = PaymentGateway.objects.create(
payment_gateway_id=payment_gateway_id,
payment_gateway_name=payment_gateway_name,
payment_gateway_description=payment_gateway_description,
payment_gateway_url=data.get("payment_gateway_url"),
payment_gateway_api_key=payment_gateway_api_key,
payment_gateway_api_secret=payment_gateway_api_secret,
payment_gateway_api_url=data.get("payment_gateway_api_url"),
payment_gateway_api_version=payment_gateway_api_version,
payment_gateway_api_method=payment_gateway_api_method,
is_active=data.get("is_active", True),
gateway_priority=data.get("gateway_priority", 0),
)
# Handle logo upload if provided
if "payment_gateway_logo" in request.FILES:
gateway.payment_gateway_logo = request.FILES["payment_gateway_logo"]
gateway.save()
return JsonResponse(
{"status": "success", "payment_gateway": _payment_gateway_to_dict(gateway, request)},
status=201,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PaymentGatewayListAPI(APIView):
"""
List PaymentGateways, optionally filtered by is_active.
Body: token, username, is_active (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
qs = PaymentGateway.objects.all().order_by("-gateway_priority", "-created_date")
is_active = data.get("is_active")
if is_active is not None:
qs = qs.filter(is_active=bool(is_active))
gateways = [_payment_gateway_to_dict(g, request) for g in qs]
return JsonResponse({"status": "success", "payment_gateways": gateways}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PaymentGatewayUpdateAPI(APIView):
"""
Update an existing PaymentGateway.
Body: token, username, payment_gateway_id (required);
payment_gateway_name, payment_gateway_description, payment_gateway_url,
payment_gateway_api_key, payment_gateway_api_secret, payment_gateway_api_url,
payment_gateway_api_version, payment_gateway_api_method, is_active,
gateway_priority (optional);
payment_gateway_logo (file upload, optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
payment_gateway_id = data.get("payment_gateway_id")
if not payment_gateway_id:
return JsonResponse(
{"status": "error", "message": "payment_gateway_id is required."},
status=400,
)
try:
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway_id)
except PaymentGateway.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "PaymentGateway not found."},
status=404,
)
# Update fields if provided
if data.get("payment_gateway_name") is not None:
gateway.payment_gateway_name = data["payment_gateway_name"]
if data.get("payment_gateway_description") is not None:
gateway.payment_gateway_description = data["payment_gateway_description"]
if "payment_gateway_url" in data:
gateway.payment_gateway_url = data["payment_gateway_url"] or None
if data.get("payment_gateway_api_key") is not None:
gateway.payment_gateway_api_key = data["payment_gateway_api_key"]
if data.get("payment_gateway_api_secret") is not None:
gateway.payment_gateway_api_secret = data["payment_gateway_api_secret"]
if "payment_gateway_api_url" in data:
gateway.payment_gateway_api_url = data["payment_gateway_api_url"] or None
if data.get("payment_gateway_api_version") is not None:
gateway.payment_gateway_api_version = data["payment_gateway_api_version"]
if data.get("payment_gateway_api_method") is not None:
gateway.payment_gateway_api_method = data["payment_gateway_api_method"]
if data.get("is_active") is not None:
gateway.is_active = bool(data["is_active"])
if data.get("gateway_priority") is not None:
try:
gateway.gateway_priority = int(data["gateway_priority"])
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "gateway_priority must be an integer."},
status=400,
)
# Handle logo upload if provided
if "payment_gateway_logo" in request.FILES:
gateway.payment_gateway_logo = request.FILES["payment_gateway_logo"]
gateway.save()
return JsonResponse(
{"status": "success", "payment_gateway": _payment_gateway_to_dict(gateway, request)},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PaymentGatewayDeleteAPI(APIView):
"""
Delete an existing PaymentGateway.
Body: token, username, payment_gateway_id.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
payment_gateway_id = data.get("payment_gateway_id")
if not payment_gateway_id:
return JsonResponse(
{"status": "error", "message": "payment_gateway_id is required."},
status=400,
)
try:
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway_id)
except PaymentGateway.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "PaymentGateway not found."},
status=404,
)
gateway_name = gateway.payment_gateway_name
gateway.delete()
return JsonResponse(
{"status": "success", "message": f"PaymentGateway '{gateway_name}' deleted successfully."},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
def _payment_gateway_credentials_to_dict(credentials, request=None):
"""Serialize PaymentGatewayCredentials for JSON."""
data = model_to_dict(
credentials,
fields=[
"id",
"payment_gateway_credentials_value",
"created_date",
"updated_date",
],
)
data["payment_gateway_id"] = credentials.payment_gateway_id
data["payment_gateway_name"] = credentials.payment_gateway.payment_gateway_name
data["payment_gateway_payment_gateway_id"] = credentials.payment_gateway.payment_gateway_id
return data
@method_decorator(csrf_exempt, name="dispatch")
class PaymentGatewayCredentialsCreateAPI(APIView):
"""
Create a new PaymentGatewayCredentials.
Body: token, username, payment_gateway_id (or payment_gateway_payment_gateway_id),
payment_gateway_credentials_value (required).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
payment_gateway_id = data.get("payment_gateway_id") or data.get("payment_gateway_payment_gateway_id")
payment_gateway_credentials_value = data.get("payment_gateway_credentials_value")
if not payment_gateway_id or not payment_gateway_credentials_value:
return JsonResponse(
{
"status": "error",
"message": "payment_gateway_id (or payment_gateway_payment_gateway_id) and payment_gateway_credentials_value are required.",
},
status=400,
)
try:
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway_id)
except PaymentGateway.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "PaymentGateway not found."},
status=404,
)
credentials = PaymentGatewayCredentials.objects.create(
payment_gateway=gateway,
payment_gateway_credentials_value=payment_gateway_credentials_value,
)
return JsonResponse(
{"status": "success", "credentials": _payment_gateway_credentials_to_dict(credentials, request)},
status=201,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PaymentGatewayCredentialsListAPI(APIView):
"""
List PaymentGatewayCredentials, optionally filtered by payment_gateway_id.
Body: token, username, payment_gateway_id (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
qs = PaymentGatewayCredentials.objects.select_related("payment_gateway").all().order_by("-created_date")
payment_gateway_id = data.get("payment_gateway_id") or data.get("payment_gateway_payment_gateway_id")
if payment_gateway_id:
try:
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway_id)
qs = qs.filter(payment_gateway=gateway)
except PaymentGateway.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "PaymentGateway not found."},
status=404,
)
credentials_list = [_payment_gateway_credentials_to_dict(c, request) for c in qs]
return JsonResponse({"status": "success", "credentials": credentials_list}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PaymentGatewayCredentialsUpdateAPI(APIView):
"""
Update an existing PaymentGatewayCredentials.
Body: token, username, credentials_id (required);
payment_gateway_credentials_value (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
credentials_id = data.get("credentials_id")
if not credentials_id:
return JsonResponse(
{"status": "error", "message": "credentials_id is required."},
status=400,
)
try:
credentials = PaymentGatewayCredentials.objects.select_related("payment_gateway").get(id=credentials_id)
except PaymentGatewayCredentials.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "PaymentGatewayCredentials not found."},
status=404,
)
# Update credentials value if provided
if data.get("payment_gateway_credentials_value") is not None:
credentials.payment_gateway_credentials_value = data["payment_gateway_credentials_value"]
credentials.save()
return JsonResponse(
{"status": "success", "credentials": _payment_gateway_credentials_to_dict(credentials, request)},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class PaymentGatewayCredentialsDeleteAPI(APIView):
"""
Delete an existing PaymentGatewayCredentials.
Body: token, username, credentials_id.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
credentials_id = data.get("credentials_id")
if not credentials_id:
return JsonResponse(
{"status": "error", "message": "credentials_id is required."},
status=400,
)
try:
credentials = PaymentGatewayCredentials.objects.select_related("payment_gateway").get(id=credentials_id)
except PaymentGatewayCredentials.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "PaymentGatewayCredentials not found."},
status=404,
)
gateway_name = credentials.payment_gateway.payment_gateway_name
credentials.delete()
return JsonResponse(
{
"status": "success",
"message": f"PaymentGatewayCredentials for '{gateway_name}' deleted successfully.",
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BankingOperationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'banking_operations'

View File

@@ -0,0 +1,69 @@
# Generated by Django 4.2.27 on 2026-03-13 16:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='PaymentGateway',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment_gateway_id', models.CharField(max_length=250)),
('payment_gateway_name', models.CharField(max_length=250)),
('payment_gateway_description', models.TextField()),
('payment_gateway_logo', models.ImageField(blank=True, null=True, upload_to='payment_gateways/')),
('payment_gateway_url', models.URLField(blank=True, null=True)),
('payment_gateway_api_key', models.CharField(max_length=250)),
('payment_gateway_api_secret', models.CharField(max_length=250)),
('payment_gateway_api_url', models.URLField(blank=True, null=True)),
('payment_gateway_api_version', models.CharField(max_length=250)),
('payment_gateway_api_method', models.CharField(max_length=250)),
('is_active', models.BooleanField(default=True)),
('created_date', models.DateField(auto_now_add=True)),
('updated_date', models.DateField(auto_now=True)),
('gateway_priority', models.IntegerField(default=0)),
],
),
migrations.CreateModel(
name='PaymentTransaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment_transaction_id', models.CharField(max_length=250)),
('payment_type', models.CharField(choices=[('credit', 'Credit'), ('debit', 'Debit'), ('transfer', 'Transfer'), ('other', 'Other')], max_length=250)),
('payment_sub_type', models.CharField(choices=[('online', 'Online'), ('offline', 'Offline'), ('other', 'Other')], max_length=250)),
('payment_transaction_amount', models.DecimalField(decimal_places=2, max_digits=10)),
('payment_transaction_currency', models.CharField(max_length=10)),
('payment_transaction_status', models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], max_length=250)),
('payment_transaction_date', models.DateField(auto_now_add=True)),
('payment_transaction_time', models.TimeField(auto_now_add=True)),
('payment_transaction_notes', models.TextField(blank=True, null=True)),
('payment_transaction_raw_data', models.JSONField(blank=True, null=True)),
('payment_transaction_response', models.JSONField(blank=True, null=True)),
('payment_transaction_error', models.JSONField(blank=True, null=True)),
('last_updated_date', models.DateField(blank=True, null=True)),
('last_updated_time', models.TimeField(blank=True, null=True)),
('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('payment_gateway', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='banking_operations.paymentgateway')),
],
),
migrations.CreateModel(
name='PaymentGatewayCredentials',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment_gateway_credentials_value', models.TextField()),
('created_date', models.DateField(auto_now_add=True)),
('updated_date', models.DateField(auto_now=True)),
('payment_gateway', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='banking_operations.paymentgateway')),
],
),
]

View File

@@ -0,0 +1,85 @@
from django.db import models
from django.contrib.auth import get_user_model
import uuid
User = get_user_model()
# Create your models here.
class PaymentGateway(models.Model):
payment_gateway_id = models.CharField(max_length=250)
payment_gateway_name = models.CharField(max_length=250)
payment_gateway_description = models.TextField()
payment_gateway_logo = models.ImageField(upload_to='payment_gateways/', blank=True, null=True)
payment_gateway_url = models.URLField(blank=True, null=True)
payment_gateway_api_key = models.CharField(max_length=250)
payment_gateway_api_secret = models.CharField(max_length=250)
payment_gateway_api_url = models.URLField(blank=True, null=True)
payment_gateway_api_version = models.CharField(max_length=250)
payment_gateway_api_method = models.CharField(max_length=250)
is_active = models.BooleanField(default=True)
created_date = models.DateField(auto_now_add=True)
updated_date = models.DateField(auto_now=True)
gateway_priority = models.IntegerField(default=0)
def __str__(self):
return self.payment_gateway_name
def __save__(self):
if not self.payment_gateway_id:
self.payment_gateway_id = str(uuid.uuid4().hex[:10]).upper()
super().save(*args, **kwargs)
class PaymentGatewayCredentials(models.Model):
payment_gateway = models.ForeignKey(PaymentGateway, on_delete=models.CASCADE)
payment_gateway_credentials_value = models.TextField()
created_date = models.DateField(auto_now_add=True)
updated_date = models.DateField(auto_now=True)
def __str__(self):
return self.payment_gateway.payment_gateway_name + " - " + self.payment_gateway_credentials_value
class PaymentTransaction(models.Model):
payment_transaction_id = models.CharField(max_length=250)
payment_type = models.CharField(max_length=250, choices=[
('credit', 'Credit'),
('debit', 'Debit'),
('transfer', 'Transfer'),
('other', 'Other'),
])
payment_sub_type = models.CharField(max_length=250, choices=[
('online', 'Online'),
('offline', 'Offline'),
('other', 'Other'),
])
payment_gateway = models.ForeignKey(PaymentGateway, on_delete=models.CASCADE)
payment_transaction_amount = models.DecimalField(max_digits=10, decimal_places=2)
payment_transaction_currency = models.CharField(max_length=10)
payment_transaction_status = models.CharField(max_length=250, choices=[
('pending', 'Pending'),
('completed', 'Completed'),
('failed', 'Failed'),
('refunded', 'Refunded'),
('cancelled', 'Cancelled'),
])
payment_transaction_date = models.DateField(auto_now_add=True)
payment_transaction_time = models.TimeField(auto_now_add=True)
payment_transaction_notes = models.TextField(blank=True, null=True)
payment_transaction_raw_data = models.JSONField(blank=True, null=True)
payment_transaction_response = models.JSONField(blank=True, null=True)
payment_transaction_error = models.JSONField(blank=True, null=True)
last_updated_date = models.DateField(blank=True, null=True)
last_updated_time = models.TimeField(blank=True, null=True)
last_updated_by = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
def __str__(self):
return self.payment_gateway.payment_gateway_name + " - " + self.payment_transaction_id
def __save__(self):
if not self.payment_transaction_id:
self.payment_transaction_id = str(self.payment_gateway.payment_gateway_name[:3].upper()) + str(uuid.uuid4().hex[:10]).upper()
super().save(*args, **kwargs)

View File

@@ -0,0 +1,170 @@
"""
Banking/payment services. transaction_initiate is called by checkout (and others)
to start a payment flow. Replace the stub with real gateway integration (e.g. Razorpay).
"""
from decimal import Decimal
from django.contrib.auth import get_user_model
from banking_operations.models import PaymentTransaction, PaymentGateway
User = get_user_model()
def transaction_initiate(
request,
user,
amount,
currency="INR",
reference_type="checkout",
reference_id=None,
bookings=None,
extra_data=None,
):
"""
Initiate a payment transaction (e.g. create Razorpay order and return payment URL).
Args:
request: Django request (for building URLs, gateway config, etc.).
user: User instance (customer).
amount: Total amount in payment currency (Decimal or float).
currency: Currency code, e.g. "INR".
reference_type: Application reference type, e.g. "checkout".
reference_id: Application reference id (e.g. booking_ids or order id).
bookings: Optional list of Booking instances or IDs linked to this transaction.
extra_data: Optional dict for gateway-specific data.
Returns:
dict: On success: {"success": True, "transaction_id": "...", "payment_url": "...", "message": "..."}
On failure: {"success": False, "message": "..."}
"""
# Stub: replace with real gateway call when banking_operations payment flow is implemented.
return {
"success": True,
"message": "Transaction initiation stub",
"transaction_id": None,
"payment_url": None,
}
def create_payment_transaction(
payment_type,
payment_sub_type,
payment_gateway,
transaction_amount,
currency="INR",
notes=None,
raw_data=None,
user=None,
):
"""
Create a PaymentTransaction with pending status.
Args:
payment_type: Payment type - 'credit', 'debit', 'transfer', or 'other'
payment_sub_type: Payment sub type - 'online', 'offline', or 'other'
payment_gateway: PaymentGateway instance or payment_gateway_id (str)
transaction_amount: Transaction amount (Decimal, float, or string)
currency: Currency code, e.g. "INR" (default: "INR")
notes: Optional transaction notes (str)
raw_data: Optional raw data dict to store in payment_transaction_raw_data (dict)
user: Optional User instance for last_updated_by
Returns:
tuple: (success: bool, transaction: PaymentTransaction or None, error_message: str or None)
"""
try:
# Validate payment_type
valid_payment_types = ['credit', 'debit', 'transfer', 'other']
if payment_type not in valid_payment_types:
return False, None, f"Invalid payment_type. Must be one of: {', '.join(valid_payment_types)}"
# Validate payment_sub_type
valid_payment_sub_types = ['online', 'offline', 'other']
if payment_sub_type not in valid_payment_sub_types:
return False, None, f"Invalid payment_sub_type. Must be one of: {', '.join(valid_payment_sub_types)}"
# Get PaymentGateway instance
if isinstance(payment_gateway, PaymentGateway):
gateway = payment_gateway
elif isinstance(payment_gateway, str):
try:
gateway = PaymentGateway.objects.get(payment_gateway_id=payment_gateway)
except PaymentGateway.DoesNotExist:
return False, None, f"PaymentGateway with id '{payment_gateway}' not found."
else:
return False, None, "payment_gateway must be a PaymentGateway instance or payment_gateway_id string."
# Validate transaction_amount
try:
amount = Decimal(str(transaction_amount))
if amount <= 0:
return False, None, "transaction_amount must be greater than zero."
except (ValueError, TypeError):
return False, None, "transaction_amount must be a valid number."
# Validate currency
if not currency or len(currency) > 10:
return False, None, "currency must be a valid currency code (max 10 characters)."
# Create PaymentTransaction
transaction = PaymentTransaction.objects.create(
payment_type=payment_type,
payment_sub_type=payment_sub_type,
payment_gateway=gateway,
payment_transaction_amount=amount,
payment_transaction_currency=currency,
payment_transaction_status='pending', # Initial state as requested
payment_transaction_notes=notes,
payment_transaction_raw_data=raw_data,
last_updated_by=user if isinstance(user, User) else None,
)
return True, transaction.payment_transaction_id, None
except Exception as e:
return False, None, str(e)
def update_payment_transaction(
payment_transaction_id,
payment_transaction_status,
payment_transaction_notes=None,
payment_transaction_raw_data=None,
payment_transaction_response=None,
payment_transaction_error=None,
user=None,
):
"""
Update a PaymentTransaction with the given status and notes.
Args:
payment_transaction_id: PaymentTransaction id (str)
payment_transaction_status: PaymentTransaction status - 'pending', 'completed', 'failed', 'refunded', 'cancelled'
payment_transaction_notes: Optional transaction notes (str)
payment_transaction_raw_data: Optional raw data dict to store in payment_transaction_raw_data (dict)
payment_transaction_response: Optional response dict to store in payment_transaction_response (dict)
payment_transaction_error: Optional error dict to store in payment_transaction_error (dict)
user: Optional User instance for last_updated_by
"""
try:
# Get PaymentTransaction instance
if isinstance(payment_transaction_id, PaymentTransaction):
transaction = payment_transaction_id
elif isinstance(payment_transaction_id, str):
try:
transaction = PaymentTransaction.objects.get(payment_transaction_id=payment_transaction_id)
except PaymentTransaction.DoesNotExist:
return False, None, f"PaymentTransaction with id '{payment_transaction_id}' not found."
else:
return False, None, "payment_transaction_id must be a PaymentTransaction instance or payment_transaction_id string."
# Update PaymentTransaction
transaction.payment_transaction_status = payment_transaction_status
transaction.payment_transaction_notes = payment_transaction_notes
transaction.payment_transaction_raw_data = payment_transaction_raw_data
transaction.payment_transaction_response = payment_transaction_response
transaction.payment_transaction_error = payment_transaction_error
transaction.last_updated_by = user if isinstance(user, User) else None
transaction.save()
return True, transaction.payment_transaction_id, None
except Exception as e:
return False, None, str(e)

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,24 @@
from django.urls import path
from banking_operations.api import (
PaymentGatewayCreateAPI,
PaymentGatewayListAPI,
PaymentGatewayUpdateAPI,
PaymentGatewayDeleteAPI,
PaymentGatewayCredentialsCreateAPI,
PaymentGatewayCredentialsListAPI,
PaymentGatewayCredentialsUpdateAPI,
PaymentGatewayCredentialsDeleteAPI,
)
urlpatterns = [
path("payment-gateway/create/", PaymentGatewayCreateAPI.as_view(), name="payment_gateway_create"),
path("payment-gateway/list/", PaymentGatewayListAPI.as_view(), name="payment_gateway_list"),
path("payment-gateway/update/", PaymentGatewayUpdateAPI.as_view(), name="payment_gateway_update"),
path("payment-gateway/delete/", PaymentGatewayDeleteAPI.as_view(), name="payment_gateway_delete"),
path("payment-gateway-credentials/create/", PaymentGatewayCredentialsCreateAPI.as_view(), name="payment_gateway_credentials_create"),
path("payment-gateway-credentials/list/", PaymentGatewayCredentialsListAPI.as_view(), name="payment_gateway_credentials_list"),
path("payment-gateway-credentials/update/", PaymentGatewayCredentialsUpdateAPI.as_view(), name="payment_gateway_credentials_update"),
path("payment-gateway-credentials/delete/", PaymentGatewayCredentialsDeleteAPI.as_view(), name="payment_gateway_credentials_delete"),
]

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,101 @@
# Generated by Django 4.2.27 on 2026-03-13 16:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('events', '0007_event_gst_percentage_1_event_gst_percentage_2_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Booking',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('booking_id', models.CharField(max_length=250)),
('quantity', models.IntegerField()),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('created_date', models.DateField(auto_now_add=True)),
('updated_date', models.DateField(auto_now=True)),
('transaction_id', models.CharField(blank=True, max_length=250, null=True)),
],
),
migrations.CreateModel(
name='TicketMeta',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket_name', models.CharField(max_length=250)),
('maximum_quantity', models.IntegerField()),
('available_quantity', models.IntegerField(default=0)),
('is_active', models.BooleanField(default=True)),
('created_date', models.DateField(auto_now_add=True)),
('updated_date', models.DateField(auto_now=True)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event')),
],
),
migrations.CreateModel(
name='TicketType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket_type', models.CharField(max_length=250)),
('ticket_type_description', models.TextField()),
('ticket_type_quantity', models.IntegerField()),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('is_active', models.BooleanField(default=True)),
('created_date', models.DateField(auto_now_add=True)),
('updated_date', models.DateField(auto_now=True)),
('is_offer', models.BooleanField(default=False)),
('offer_percentage', models.IntegerField(default=0)),
('offer_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('offer_start_date', models.DateField(blank=True, null=True)),
('offer_end_date', models.DateField(blank=True, null=True)),
('ticket_meta', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.ticketmeta')),
],
),
migrations.CreateModel(
name='Ticket',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket_id', models.CharField(max_length=250)),
('is_checked_in', models.BooleanField(default=False)),
('checked_in_date_time', models.DateTimeField(blank=True, null=True)),
('booking', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.booking')),
],
),
migrations.CreateModel(
name='Cart',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.IntegerField()),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('is_active', models.BooleanField(default=True)),
('created_date', models.DateField(auto_now_add=True)),
('updated_date', models.DateField(auto_now=True)),
('ticket_meta', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.ticketmeta')),
('ticket_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.tickettype')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='booking',
name='ticket_meta',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.ticketmeta'),
),
migrations.AddField(
model_name='booking',
name='ticket_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookings.tickettype'),
),
migrations.AddField(
model_name='booking',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -4,10 +4,9 @@ from events.models import Event
from accounts.models import User
# Create your models here.
class Ticket(models.Model):
class TicketMeta(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
ticket_name = models.CharField(max_length=250)
price_per_ticket = models.DecimalField(max_digits=10, decimal_places=2)
maximum_quantity = models.IntegerField()
available_quantity = models.IntegerField(default=0)
is_active = models.BooleanField(default=True)
@@ -19,11 +18,13 @@ class Ticket(models.Model):
class TicketType(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
ticket_meta = models.ForeignKey(TicketMeta, on_delete=models.CASCADE)
ticket_type = models.CharField(max_length=250)
ticket_type_description = models.TextField()
quantity = models.IntegerField()
ticket_type_quantity = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
is_active = models.BooleanField(default=True)
created_date = models.DateField(auto_now_add=True)
updated_date = models.DateField(auto_now=True)
@@ -41,10 +42,11 @@ class TicketType(models.Model):
class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
ticket_meta = models.ForeignKey(TicketMeta, on_delete=models.CASCADE)
ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE)
quantity = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
is_active = models.BooleanField(default=True)
created_date = models.DateField(auto_now_add=True)
updated_date = models.DateField(auto_now=True)
@@ -55,7 +57,7 @@ class Cart(models.Model):
class Booking(models.Model):
booking_id = models.CharField(max_length=250)
user = models.ForeignKey(User, on_delete=models.CASCADE)
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
ticket_meta = models.ForeignKey(TicketMeta, on_delete=models.CASCADE)
ticket_type = models.ForeignKey(TicketType, on_delete=models.CASCADE)
quantity = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
@@ -71,3 +73,25 @@ class Booking(models.Model):
def __str__(self):
return self.booking_id
class Ticket(models.Model):
booking = models.ForeignKey(Booking, on_delete=models.CASCADE)
ticket_id = models.CharField(max_length=250)
is_checked_in = models.BooleanField(default=False)
checked_in_date_time = models.DateTimeField(blank=True, null=True)
def __save__(self):
if not self.ticket_id:
self.ticket_id = str(self.booking.ticket_meta.event.name[:3].upper()) + str(uuid.uuid4().hex[:10]).upper()
super().save(*args, **kwargs)
def __str__(self):
return self.ticket_id
def check_in(self, ticket_id):
if self.ticket_id == ticket_id:
self.is_checked_in = True
self.checked_in_date_time = datetime.now()
self.save()
return True
return False

61
bookings/services.py Normal file
View File

@@ -0,0 +1,61 @@
from typing import List
import uuid
from django.utils import timezone
from bookings.models import Booking, Ticket
def _generate_ticket_id(booking: Booking) -> str:
"""
Generate a ticket_id based on the event name and a random UUID segment.
Pattern: <EVT><RANDOM_HEX>
- EVT: first 3 characters of event name (uppercase), or 'EVT' fallback
- RANDOM_HEX: first 10 chars of uuid4 hex (uppercase)
"""
event = getattr(booking.ticket_meta, "event", None)
if event and getattr(event, "name", None):
prefix = (event.name or "EVT")[:3].upper()
else:
prefix = "EVT"
return prefix + uuid.uuid4().hex[:10].upper()
def generate_tickets_for_booking(booking: Booking) -> List[Ticket]:
"""
Generate Ticket instances for a given Booking based on its quantity.
This function does NOT perform any payment or business-rule validation.
It simply creates one Ticket per quantity on the booking.
Args:
booking: Booking instance for which tickets should be generated.
Returns:
List[Ticket]: List of created Ticket instances.
"""
if not isinstance(booking, Booking):
raise TypeError("booking must be a Booking instance")
if booking.quantity <= 0:
return []
tickets: List[Ticket] = []
for _ in range(booking.quantity):
tickets.append(
Ticket(
booking=booking,
ticket_id=_generate_ticket_id(booking),
is_checked_in=False,
checked_in_date_time=None,
)
)
# Bulk create for efficiency
Ticket.objects.bulk_create(tickets)
# Refresh from DB to ensure we have primary keys and any defaults
return list[Ticket](Ticket.objects.filter(booking=booking).order_by("id"))

View File

@@ -0,0 +1,2 @@
from . import ticket_meta_type
from . import booking_api

View File

@@ -1,301 +0,0 @@
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 rest_framework.views import APIView
from bookings.models import Ticket
from events.models import Event
from mobile_api.utils import validate_token_and_get_user
def _ticket_to_dict(ticket):
"""
Helper to serialise a Ticket instance into a simple dict suitable for JSON.
"""
data = model_to_dict(
ticket,
fields=[
"id",
"ticket_name",
"price_per_ticket",
"maximum_quantity",
"available_quantity",
"is_active",
"created_date",
"updated_date",
],
)
data["event_id"] = ticket.event_id
return data
@method_decorator(csrf_exempt, name="dispatch")
class TicketCreateAPI(APIView):
"""
Create a new Ticket.
Expected JSON body (along with token & username used across mobile_api):
{
"token": "...",
"username": "...",
"event_id": 1,
"ticket_name": "VIP",
"price_per_ticket": 1000.0,
"maximum_quantity": 50,
"available_quantity": 50, # optional, defaults to maximum_quantity
"is_active": true # optional, defaults to true
}
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
event_id = data.get("event_id")
ticket_name = data.get("ticket_name")
price_per_ticket = data.get("price_per_ticket")
maximum_quantity = data.get("maximum_quantity")
available_quantity = data.get("available_quantity")
is_active = data.get("is_active", True)
if not event_id or not ticket_name or price_per_ticket is None or maximum_quantity is None:
return JsonResponse(
{
"status": "error",
"message": "event_id, ticket_name, price_per_ticket and maximum_quantity are required.",
},
status=400,
)
try:
event = Event.objects.get(id=event_id)
except Event.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "Event not found."},
status=404,
)
try:
price_per_ticket = float(price_per_ticket)
maximum_quantity = int(maximum_quantity)
if available_quantity is not None:
available_quantity = int(available_quantity)
else:
available_quantity = maximum_quantity
except (TypeError, ValueError):
return JsonResponse(
{
"status": "error",
"message": "price_per_ticket, maximum_quantity and available_quantity must be numeric.",
},
status=400,
)
if maximum_quantity <= 0:
return JsonResponse(
{
"status": "error",
"message": "maximum_quantity must be greater than zero.",
},
status=400,
)
ticket = Ticket.objects.create(
event=event,
ticket_name=ticket_name,
price_per_ticket=price_per_ticket,
maximum_quantity=maximum_quantity,
available_quantity=available_quantity,
is_active=bool(is_active),
)
return JsonResponse(
{"status": "success", "ticket": _ticket_to_dict(ticket)},
status=201,
)
except Exception as e:
return JsonResponse(
{"status": "error", "message": str(e)},
status=500,
)
@method_decorator(csrf_exempt, name="dispatch")
class TicketListAPI(APIView):
"""
List tickets, optionally filtered by event_id.
Expected JSON body:
{
"token": "...",
"username": "...",
"event_id": 1 # optional
}
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
event_id = data.get("event_id")
tickets_qs = Ticket.objects.all().order_by("-created_date")
if event_id:
tickets_qs = tickets_qs.filter(event_id=event_id)
tickets = [_ticket_to_dict(t) for t in tickets_qs]
return JsonResponse(
{"status": "success", "tickets": tickets},
status=200,
)
except Exception as e:
return JsonResponse(
{"status": "error", "message": str(e)},
status=500,
)
@method_decorator(csrf_exempt, name="dispatch")
class TicketUpdateAPI(APIView):
"""
Update an existing Ticket.
Expected JSON body:
{
"token": "...",
"username": "...",
"ticket_id": 1,
"ticket_name": "...", # optional
"price_per_ticket": 1000.0, # optional
"maximum_quantity": 50, # optional
"available_quantity": 50, # optional
"is_active": true # optional
}
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
ticket_id = data.get("ticket_id")
if not ticket_id:
return JsonResponse(
{"status": "error", "message": "ticket_id is required."},
status=400,
)
try:
ticket = Ticket.objects.get(id=ticket_id)
except Ticket.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "Ticket not found."},
status=404,
)
# Optional updates
ticket_name = data.get("ticket_name")
price_per_ticket = data.get("price_per_ticket")
maximum_quantity = data.get("maximum_quantity")
available_quantity = data.get("available_quantity")
is_active = data.get("is_active")
if ticket_name is not None:
ticket.ticket_name = ticket_name
try:
if price_per_ticket is not None:
ticket.price_per_ticket = float(price_per_ticket)
if maximum_quantity is not None:
maximum_quantity = int(maximum_quantity)
if maximum_quantity <= 0:
return JsonResponse(
{
"status": "error",
"message": "maximum_quantity must be greater than zero.",
},
status=400,
)
ticket.maximum_quantity = maximum_quantity
if available_quantity is not None:
ticket.available_quantity = int(available_quantity)
except (TypeError, ValueError):
return JsonResponse(
{
"status": "error",
"message": "price_per_ticket, maximum_quantity and available_quantity must be numeric.",
},
status=400,
)
if is_active is not None:
ticket.is_active = bool(is_active)
ticket.save()
return JsonResponse(
{"status": "success", "ticket": _ticket_to_dict(ticket)},
status=200,
)
except Exception as e:
return JsonResponse(
{"status": "error", "message": str(e)},
status=500,
)
@method_decorator(csrf_exempt, name="dispatch")
class TicketDeleteAPI(APIView):
"""
Delete an existing Ticket.
Expected JSON body:
{
"token": "...",
"username": "...",
"ticket_id": 1
}
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
ticket_id = data.get("ticket_id")
if not ticket_id:
return JsonResponse(
{"status": "error", "message": "ticket_id is required."},
status=400,
)
try:
ticket = Ticket.objects.get(id=ticket_id)
except Ticket.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "Ticket not found."},
status=404,
)
ticket.delete()
return JsonResponse(
{"status": "success", "message": "Ticket deleted successfully."},
status=200,
)
except Exception as e:
return JsonResponse(
{"status": "error", "message": str(e)},
status=500,
)

View File

@@ -0,0 +1,369 @@
import uuid
from decimal import Decimal
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.utils import timezone
from datetime import date
from rest_framework.views import APIView
from bookings.models import Cart, TicketType, TicketMeta, Booking, Ticket
from mobile_api.utils import validate_token_and_get_user
from banking_operations.services import transaction_initiate
from eventify_logger.services import log
def _cart_to_dict(cart):
"""Serialize Cart for JSON."""
data = model_to_dict(
cart,
fields=["id", "quantity", "price", "created_date", "updated_date"],
)
data["user_id"] = cart.user_id
data["ticket_meta_id"] = cart.ticket_meta_id
data["ticket_type_id"] = cart.ticket_type_id
return data
@method_decorator(csrf_exempt, name="dispatch")
class AddToCartAPI(APIView):
"""
Add TicketType to Cart (when customer clicks plus button).
Body: token, username, ticket_type_id, quantity (optional, defaults to 1).
If cart item already exists for this user + ticket_type, quantity is incremented.
Price is taken from TicketType (offer_price if offer is active, else price).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
ticket_type_id = data.get("ticket_type_id")
quantity = data.get("quantity", 1) # Default to 1 for plus button click
if not ticket_type_id:
return JsonResponse(
{"status": "error", "message": "ticket_type_id is required."},
status=400,
)
try:
ticket_type = TicketType.objects.select_related("ticket_meta").get(id=ticket_type_id)
except TicketType.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "TicketType not found."},
status=404,
)
if not ticket_type.is_active:
return JsonResponse(
{"status": "error", "message": "TicketType is not active."},
status=400,
)
try:
quantity = int(quantity)
if quantity <= 0:
return JsonResponse(
{"status": "error", "message": "quantity must be greater than zero."},
status=400,
)
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "quantity must be a positive integer."},
status=400,
)
# Determine price: use offer_price if offer is active, else regular price
price = ticket_type.price
if ticket_type.is_offer:
# Check if offer is currently valid (if dates are set)
today = date.today()
offer_valid = True
if ticket_type.offer_start_date and ticket_type.offer_start_date > today:
offer_valid = False
if ticket_type.offer_end_date and ticket_type.offer_end_date < today:
offer_valid = False
if offer_valid and ticket_type.offer_price > 0:
price = ticket_type.offer_price
# Check if cart item already exists for this user + ticket_type
cart_item, created = Cart.objects.get_or_create(
user=user,
ticket_type=ticket_type,
defaults={
"ticket_meta": ticket_type.ticket_meta,
"quantity": quantity,
"price": price,
},
)
if not created:
# Update existing cart item: increment quantity
cart_item.quantity += quantity
cart_item.price = price # Update price in case offer changed
cart_item.save()
return JsonResponse(
{
"status": "success",
"message": "TicketType added to cart." if created else "Cart item updated.",
"cart_item": _cart_to_dict(cart_item),
},
status=201 if created else 200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class DeleteFromCartAPI(APIView):
"""
Remove or decrement TicketType from Cart (when customer clicks minus button).
Body: token, username, ticket_type_id, quantity (optional, defaults to 1).
Decrements quantity by 1 (or by given quantity). If quantity becomes 0 or less,
the cart item is deleted.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
ticket_type_id = data.get("ticket_type_id")
quantity = data.get("quantity", 1) # Default to 1 for minus button click
if not ticket_type_id:
return JsonResponse(
{"status": "error", "message": "ticket_type_id is required."},
status=400,
)
try:
quantity = int(quantity)
if quantity <= 0:
return JsonResponse(
{"status": "error", "message": "quantity must be greater than zero."},
status=400,
)
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "quantity must be a positive integer."},
status=400,
)
try:
cart_item = Cart.objects.get(user=user, ticket_type_id=ticket_type_id)
except Cart.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "Cart item not found for this ticket type."},
status=404,
)
cart_item.quantity -= quantity
if cart_item.quantity <= 0:
cart_item.delete()
return JsonResponse(
{
"status": "success",
"message": "TicketType removed from cart.",
"cart_item": None,
"removed": True,
},
status=200,
)
cart_item.save()
return JsonResponse(
{
"status": "success",
"message": "Cart quantity updated.",
"cart_item": _cart_to_dict(cart_item),
"removed": False,
},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class CheckoutAPI(APIView):
"""
Checkout the authenticated user's cart: create one Booking per cart line,
call transaction_initiate in banking_operations, then clear the checked-out cart items.
Body: token, username.
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
cart_items = list(
Cart.objects.filter(user=user, is_active=True).select_related(
"ticket_meta", "ticket_type", "ticket_meta__event"
)
)
if not cart_items:
return JsonResponse(
{"status": "error", "message": "Cart is empty. Add ticket types before checkout."},
status=400,
)
total_amount = Decimal("0")
created_bookings = []
cart_ids_to_clear = []
for item in cart_items:
event_name = (
(item.ticket_meta.event.name or "EVT")[:3].upper()
if item.ticket_meta.event_id else "EVT"
)
booking_id = event_name + uuid.uuid4().hex[:10].upper()
line_total = Decimal(str(item.price)) * item.quantity
total_amount += line_total
booking = Booking.objects.create(
booking_id=booking_id,
user=user,
ticket_meta=item.ticket_meta,
ticket_type=item.ticket_type,
quantity=item.quantity,
price=item.price,
)
created_bookings.append(booking)
cart_ids_to_clear.append(item.id)
reference_id = ",".join(b.booking_id for b in created_bookings)
result = transaction_initiate(
request=request,
user=user,
amount=float(total_amount),
currency="INR",
reference_type="checkout",
reference_id=reference_id,
bookings=created_bookings,
extra_data=None,
)
if not result.get("success"):
for b in created_bookings:
b.delete()
return JsonResponse(
{
"status": "error",
"message": result.get("message", "Transaction initiation failed."),
},
status=502,
)
transaction_id = result.get("transaction_id")
if transaction_id:
for b in created_bookings:
b.transaction_id = transaction_id
b.save(update_fields=["transaction_id"])
Cart.objects.filter(id__in=cart_ids_to_clear).delete()
log("info", "Checkout complete", request=request, user=user, logger_data={
"booking_ids": [b.booking_id for b in created_bookings],
"total_amount": str(total_amount),
})
response_payload = {
"status": "success",
"message": "Checkout complete. Proceed to payment.",
"booking_ids": [b.booking_id for b in created_bookings],
"total_amount": str(total_amount),
"transaction_id": result.get("transaction_id"),
"payment_url": result.get("payment_url"),
}
return JsonResponse(response_payload, status=200)
except Exception as e:
log("error", "Checkout exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class CheckInAPI(APIView):
"""
Check-in a ticket by scanning QR code (ticket_id).
Body: token, username, ticket_id (required).
Looks up Ticket by ticket_id. If found and not already checked in,
sets is_checked_in=True and checked_in_date_time=now. Returns success or
appropriate error (ticket not found / already checked in).
"""
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
ticket_id = data.get("ticket_id")
if not ticket_id or not str(ticket_id).strip():
return JsonResponse(
{"status": "error", "message": "ticket_id is required."},
status=400,
)
ticket_id = str(ticket_id).strip()
try:
ticket = Ticket.objects.select_related(
"booking", "booking__ticket_meta", "booking__ticket_meta__event", "booking__ticket_type"
).get(ticket_id=ticket_id)
except Ticket.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "Ticket not found."},
status=404,
)
if ticket.is_checked_in:
log("info", "Check-in duplicate - ticket already checked in", request=request, user=user, logger_data={"ticket_id": ticket_id})
return JsonResponse(
{
"status": "success",
"message": "Ticket already checked in.",
"ticket_id": ticket.ticket_id,
"is_checked_in": True,
"checked_in_date_time": ticket.checked_in_date_time.isoformat() if ticket.checked_in_date_time else None,
"event_name": ticket.booking.ticket_meta.event.name if ticket.booking.ticket_meta_id else None,
"booking_id": ticket.booking.booking_id,
},
status=200,
)
ticket.is_checked_in = True
ticket.checked_in_date_time = timezone.now()
ticket.save(update_fields=["is_checked_in", "checked_in_date_time"])
log("info", "Check-in successful", request=request, user=user, logger_data={"ticket_id": ticket_id, "booking_id": ticket.booking.booking_id})
return JsonResponse(
{
"status": "success",
"message": "Check-in successful.",
"ticket_id": ticket.ticket_id,
"is_checked_in": True,
"checked_in_date_time": ticket.checked_in_date_time.isoformat(),
"event_name": ticket.booking.ticket_meta.event.name if ticket.booking.ticket_meta_id else None,
"booking_id": ticket.booking.booking_id,
},
status=200,
)
except Exception as e:
log("error", "Check-in exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)

View File

@@ -0,0 +1,489 @@
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 rest_framework.views import APIView
from bookings.models import TicketMeta, TicketType
from events.models import Event
from mobile_api.utils import validate_token_and_get_user
def _ticket_meta_to_dict(meta):
"""Serialize TicketMeta for JSON."""
data = model_to_dict(
meta,
fields=[
"id",
"ticket_name",
"maximum_quantity",
"available_quantity",
"is_active",
"created_date",
"updated_date",
],
)
data["event_id"] = meta.event_id
return data
def _ticket_type_to_dict(tt):
"""Serialize TicketType for JSON."""
data = model_to_dict(
tt,
fields=[
"id",
"ticket_type",
"ticket_type_description",
"quantity",
"price",
"is_active",
"created_date",
"updated_date",
"is_offer",
"offer_percentage",
"offer_price",
"offer_start_date",
"offer_end_date",
],
)
data["ticket_meta_id"] = tt.ticket_meta_id
return data
# ---------- TicketMeta (event-level ticket config) CRUD ----------
@method_decorator(csrf_exempt, name="dispatch")
class TicketMetaCreateAPI(APIView):
"""
Create a new TicketMeta (event-level ticket config).
Body: token, username, event_id, ticket_name, maximum_quantity,
available_quantity (optional, defaults to maximum_quantity), is_active (optional, default true).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
event_id = data.get("event_id")
ticket_name = data.get("ticket_name")
maximum_quantity = data.get("maximum_quantity")
available_quantity = data.get("available_quantity")
is_active = data.get("is_active", True)
if not event_id or not ticket_name or maximum_quantity is None:
return JsonResponse(
{
"status": "error",
"message": "event_id, ticket_name and maximum_quantity are required.",
},
status=400,
)
try:
event = Event.objects.get(id=event_id)
except Event.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "Event not found."},
status=404,
)
try:
maximum_quantity = int(maximum_quantity)
available_quantity = int(available_quantity) if available_quantity is not None else maximum_quantity
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "maximum_quantity and available_quantity must be integers."},
status=400,
)
if maximum_quantity <= 0:
return JsonResponse(
{"status": "error", "message": "maximum_quantity must be greater than zero."},
status=400,
)
meta = TicketMeta.objects.create(
event=event,
ticket_name=ticket_name,
maximum_quantity=maximum_quantity,
available_quantity=available_quantity,
is_active=bool(is_active),
)
return JsonResponse(
{"status": "success", "ticket_meta": _ticket_meta_to_dict(meta)},
status=201,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class TicketMetaListAPI(APIView):
"""List TicketMeta, optionally filtered by event_id. Body: token, username, event_id (optional)."""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
event_id = data.get("event_id")
qs = TicketMeta.objects.filter(is_active=True).order_by("-created_date")
if event_id:
qs = qs.filter(event_id=event_id)
items = [_ticket_meta_to_dict(m) for m in qs]
return JsonResponse({"status": "success", "ticket_metas": items}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class TicketMetaUpdateAPI(APIView):
"""
Update TicketMeta. Body: token, username, ticket_meta_id (required);
ticket_name, maximum_quantity, available_quantity, is_active (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
pk = data.get("ticket_meta_id")
if not pk:
return JsonResponse({"status": "error", "message": "ticket_meta_id is required."}, status=400)
try:
meta = TicketMeta.objects.get(id=pk)
except TicketMeta.DoesNotExist:
return JsonResponse({"status": "error", "message": "TicketMeta not found."}, status=404)
if data.get("ticket_name") is not None:
meta.ticket_name = data["ticket_name"]
if data.get("maximum_quantity") is not None:
try:
val = int(data["maximum_quantity"])
if val <= 0:
return JsonResponse(
{"status": "error", "message": "maximum_quantity must be greater than zero."},
status=400,
)
meta.maximum_quantity = val
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "maximum_quantity must be an integer."},
status=400,
)
if data.get("available_quantity") is not None:
try:
meta.available_quantity = int(data["available_quantity"])
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "available_quantity must be an integer."},
status=400,
)
if data.get("is_active") is not None:
meta.is_active = bool(data["is_active"])
meta.save()
return JsonResponse({"status": "success", "ticket_meta": _ticket_meta_to_dict(meta)}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class TicketMetaDeleteAPI(APIView):
"""Delete TicketMeta. Body: token, username, ticket_meta_id."""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
pk = data.get("ticket_meta_id")
if not pk:
return JsonResponse({"status": "error", "message": "ticket_meta_id is required."}, status=400)
try:
meta = TicketMeta.objects.get(id=pk)
except TicketMeta.DoesNotExist:
return JsonResponse({"status": "error", "message": "TicketMeta not found."}, status=404)
meta.delete()
return JsonResponse({"status": "success", "message": "TicketMeta deleted successfully."}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class TicketMetaDeactivateAPI(APIView):
"""Deactivate a TicketMeta (set is_active=False). Body: token, username, ticket_meta_id."""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
pk = data.get("ticket_meta_id")
if not pk:
return JsonResponse({"status": "error", "message": "ticket_meta_id is required."}, status=400)
try:
meta = TicketMeta.objects.get(id=pk)
except TicketMeta.DoesNotExist:
return JsonResponse({"status": "error", "message": "TicketMeta not found."}, status=404)
meta.is_active = False
meta.save()
return JsonResponse(
{"status": "success", "message": "TicketMeta deactivated.", "ticket_meta": _ticket_meta_to_dict(meta)},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
# ---------- TicketType CRUD ----------
@method_decorator(csrf_exempt, name="dispatch")
class TicketTypeCreateAPI(APIView):
"""
Create a new TicketType.
Body: token, username, ticket_meta_id, ticket_type, ticket_type_description, quantity, price (required);
is_active (optional, default true); is_offer, offer_percentage, offer_price, offer_start_date, offer_end_date (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
ticket_meta_id = data.get("ticket_meta_id")
ticket_type_name = data.get("ticket_type")
ticket_type_description = data.get("ticket_type_description")
quantity = data.get("quantity")
price = data.get("price")
is_active = data.get("is_active", True)
is_offer = data.get("is_offer", False)
offer_percentage = data.get("offer_percentage", 0)
offer_price = data.get("offer_price", 0)
offer_start_date = data.get("offer_start_date")
offer_end_date = data.get("offer_end_date")
if not ticket_meta_id or not ticket_type_name or quantity is None or price is None:
return JsonResponse(
{
"status": "error",
"message": "ticket_meta_id, ticket_type, ticket_type_description, quantity and price are required.",
},
status=400,
)
try:
ticket_meta = TicketMeta.objects.get(id=ticket_meta_id)
except TicketMeta.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "TicketMeta not found."},
status=404,
)
try:
quantity = int(quantity)
price = float(price)
offer_percentage = int(offer_percentage) if offer_percentage is not None else 0
offer_price = float(offer_price) if offer_price is not None else 0
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "quantity, price, offer_percentage and offer_price must be numeric."},
status=400,
)
if quantity <= 0:
return JsonResponse(
{"status": "error", "message": "quantity must be greater than zero."},
status=400,
)
tt = TicketType.objects.create(
ticket_meta=ticket_meta,
ticket_type=ticket_type_name,
ticket_type_description=ticket_type_description or "",
quantity=quantity,
price=price,
is_active=bool(is_active),
is_offer=bool(is_offer),
offer_percentage=offer_percentage,
offer_price=offer_price,
offer_start_date=offer_start_date,
offer_end_date=offer_end_date,
)
return JsonResponse(
{"status": "success", "ticket_type": _ticket_type_to_dict(tt)},
status=201,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class TicketTypeListAPI(APIView):
"""List TicketType, optionally filtered by ticket_meta_id. Body: token, username, ticket_meta_id (optional)."""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
ticket_meta_id = data.get("ticket_meta_id")
qs = TicketType.objects.filter(is_active=True).order_by("-created_date")
if ticket_meta_id:
qs = qs.filter(ticket_meta_id=ticket_meta_id)
items = [_ticket_type_to_dict(tt) for tt in qs]
return JsonResponse({"status": "success", "ticket_types": items}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class TicketTypeUpdateAPI(APIView):
"""
Update TicketType. Body: token, username, ticket_type_id (required);
ticket_type, ticket_type_description, quantity, price, is_active,
is_offer, offer_percentage, offer_price, offer_start_date, offer_end_date (optional).
"""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
pk = data.get("ticket_type_id")
if not pk:
return JsonResponse({"status": "error", "message": "ticket_type_id is required."}, status=400)
try:
tt = TicketType.objects.get(id=pk)
except TicketType.DoesNotExist:
return JsonResponse({"status": "error", "message": "TicketType not found."}, status=404)
if data.get("ticket_type") is not None:
tt.ticket_type = data["ticket_type"]
if data.get("ticket_type_description") is not None:
tt.ticket_type_description = data["ticket_type_description"]
if data.get("quantity") is not None:
try:
val = int(data["quantity"])
if val <= 0:
return JsonResponse(
{"status": "error", "message": "quantity must be greater than zero."},
status=400,
)
tt.quantity = val
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "quantity must be an integer."},
status=400,
)
if data.get("price") is not None:
try:
tt.price = float(data["price"])
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "price must be numeric."},
status=400,
)
if data.get("is_active") is not None:
tt.is_active = bool(data["is_active"])
if data.get("is_offer") is not None:
tt.is_offer = bool(data["is_offer"])
if data.get("offer_percentage") is not None:
try:
tt.offer_percentage = int(data["offer_percentage"])
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "offer_percentage must be an integer."},
status=400,
)
if data.get("offer_price") is not None:
try:
tt.offer_price = float(data["offer_price"])
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "offer_price must be numeric."},
status=400,
)
if "offer_start_date" in data:
tt.offer_start_date = data["offer_start_date"]
if "offer_end_date" in data:
tt.offer_end_date = data["offer_end_date"]
tt.save()
return JsonResponse({"status": "success", "ticket_type": _ticket_type_to_dict(tt)}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class TicketTypeDeactivateAPI(APIView):
"""Deactivate a TicketType (set is_active=False). Body: token, username, ticket_type_id."""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
pk = data.get("ticket_type_id")
if not pk:
return JsonResponse({"status": "error", "message": "ticket_type_id is required."}, status=400)
try:
tt = TicketType.objects.get(id=pk)
except TicketType.DoesNotExist:
return JsonResponse({"status": "error", "message": "TicketType not found."}, status=404)
tt.is_active = False
tt.save()
return JsonResponse(
{"status": "success", "message": "TicketType deactivated.", "ticket_type": _ticket_type_to_dict(tt)},
status=200,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@method_decorator(csrf_exempt, name="dispatch")
class TicketTypeDeleteAPI(APIView):
"""Delete TicketType. Body: token, username, ticket_type_id."""
def post(self, request):
try:
user, token, data, error_response = validate_token_and_get_user(request)
if error_response:
return error_response
pk = data.get("ticket_type_id")
if not pk:
return JsonResponse({"status": "error", "message": "ticket_type_id is required."}, status=400)
try:
tt = TicketType.objects.get(id=pk)
except TicketType.DoesNotExist:
return JsonResponse({"status": "error", "message": "TicketType not found."}, status=404)
tt.delete()
return JsonResponse({"status": "success", "message": "TicketType deleted successfully."}, status=200)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)

View File

@@ -1,17 +1,34 @@
from django.urls import path
from bookings.tickets_view.api import (
TicketCreateAPI,
TicketListAPI,
TicketUpdateAPI,
TicketDeleteAPI,
from bookings.tickets_view.ticket_meta_type import (
TicketMetaCreateAPI,
TicketMetaListAPI,
TicketMetaUpdateAPI,
TicketMetaDeleteAPI,
TicketMetaDeactivateAPI,
TicketTypeCreateAPI,
TicketTypeListAPI,
TicketTypeUpdateAPI,
TicketTypeDeleteAPI,
TicketTypeDeactivateAPI,
)
from bookings.tickets_view.booking_api import AddToCartAPI, DeleteFromCartAPI, CheckoutAPI, CheckInAPI
urlpatterns = [
path("tickets/create/", TicketCreateAPI.as_view(), name="ticket_create"),
path("tickets/list/", TicketListAPI.as_view(), name="ticket_list"),
path("tickets/update/", TicketUpdateAPI.as_view(), name="ticket_update"),
path("tickets/delete/", TicketDeleteAPI.as_view(), name="ticket_delete"),
path("ticket-meta/create/", TicketMetaCreateAPI.as_view(), name="ticket_meta_create"),
path("ticket-meta/list/", TicketMetaListAPI.as_view(), name="ticket_meta_list"),
path("ticket-meta/update/", TicketMetaUpdateAPI.as_view(), name="ticket_meta_update"),
path("ticket-meta/delete/", TicketMetaDeleteAPI.as_view(), name="ticket_meta_delete"),
path("ticket-meta/deactivate/", TicketMetaDeactivateAPI.as_view(), name="ticket_meta_deactivate"),
path("ticket-type/create/", TicketTypeCreateAPI.as_view(), name="ticket_type_create"),
path("ticket-type/list/", TicketTypeListAPI.as_view(), name="ticket_type_list"),
path("ticket-type/update/", TicketTypeUpdateAPI.as_view(), name="ticket_type_update"),
path("ticket-type/delete/", TicketTypeDeleteAPI.as_view(), name="ticket_type_delete"),
path("ticket-type/deactivate/", TicketTypeDeactivateAPI.as_view(), name="ticket_type_deactivate"),
path("cart/add/", AddToCartAPI.as_view(), name="add_to_cart"),
path("cart/delete/", DeleteFromCartAPI.as_view(), name="delete_from_cart"),
path("checkout/", CheckoutAPI.as_view(), name="checkout"),
path("check-in/", CheckInAPI.as_view(), name="check_in"),
]

Binary file not shown.

View File

@@ -22,12 +22,16 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'eventify_logger',
'master_data',
'events',
'accounts',
'partner',
'templatetags',
'mobile_api',
'web_api',
'bookings',
'banking_operations',
'rest_framework',
'rest_framework.authtoken'
]
@@ -42,6 +46,7 @@ MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'eventify_logger.middleware.EventifyLoggingMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',

View File

@@ -32,6 +32,8 @@ urlpatterns = [
path('events/', include('events.urls')),
path('accounts/', include('accounts.urls')),
path('bookings/', include('bookings.urls')),
path('partner/', include('partner.urls')),
path('banking/', include('banking_operations.urls')),
path('api/', include('mobile_api.urls')),
# path('web-api/', include('web_api.urls')),
]

View File

10
eventify_logger/admin.py Normal file
View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import EventifyLogger
@admin.register(EventifyLogger)
class EventifyLoggerAdmin(admin.ModelAdmin):
list_display = ("logger_type", "logger_message", "logged_user", "logger_created_at")
list_filter = ("logger_type", "logger_created_at")
search_fields = ("logger_message",)
readonly_fields = ("logger_created_at",)

6
eventify_logger/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class EventifyLoggerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'eventify_logger'

View File

@@ -0,0 +1,28 @@
"""
Request logging middleware - logs every HTTP request to EventifyLogger.
"""
from eventify_logger.services import log
class EventifyLoggingMiddleware:
"""Log each request (method, path, status) after response."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
try:
status = getattr(response, "status_code", 0)
if 500 <= status < 600:
logger_type = "error"
elif 400 <= status < 500:
logger_type = "warning"
else:
logger_type = "info"
message = f"{request.method} {request.path} -> {status}"
logger_data = {"path": request.path, "method": request.method, "status_code": status}
log(logger_type=logger_type, logger_message=message, request=request, logger_data=logger_data)
except Exception:
pass
return response

View File

@@ -0,0 +1,31 @@
# Generated by Django 4.2.27 on 2026-03-09 04:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='EventifyLogger',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('logger_type', models.CharField(choices=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('critical', 'Critical')], max_length=250)),
('logger_message', models.TextField()),
('logger_data', models.TextField(blank=True, null=True)),
('logger_created_at', models.DateTimeField(auto_now_add=True)),
('logged_ip_address', models.GenericIPAddressField(blank=True, null=True)),
('logged_user_device', models.CharField(blank=True, max_length=250, null=True)),
('logged_user_browser', models.CharField(blank=True, max_length=250, null=True)),
('logged_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

27
eventify_logger/models.py Normal file
View File

@@ -0,0 +1,27 @@
from django.conf import settings
from django.db import models
class EventifyLogger(models.Model):
logger_type = models.CharField(max_length=250, choices=[
('info', 'Info'),
('warning', 'Warning'),
('error', 'Error'),
('critical', 'Critical'),
])
logger_message = models.TextField()
logger_data = models.TextField(blank=True, null=True)
logger_created_at = models.DateTimeField(auto_now_add=True)
logged_user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
blank=True,
null=True,
)
logged_ip_address = models.GenericIPAddressField(blank=True, null=True)
logged_user_device = models.CharField(max_length=250, blank=True, null=True)
logged_user_browser = models.CharField(max_length=250, blank=True, null=True)
def __str__(self):
user_str = str(self.logged_user) if self.logged_user else "anonymous"
return f"{user_str}-{self.logger_type} - {self.logger_message}"

View File

@@ -0,0 +1,58 @@
"""
Central logging service for EventifyLogger.
"""
import json
from eventify_logger.models import EventifyLogger
def _get_client_ip(request):
"""Extract client IP from request."""
if not request:
return None
x_forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded:
return x_forwarded.split(",")[0].strip() or None
return request.META.get("REMOTE_ADDR")
def _get_user_agent(request):
"""Extract User-Agent from request."""
if not request:
return None
return request.META.get("HTTP_USER_AGENT", "")
def log(logger_type, logger_message, request=None, user=None, logger_data=None):
"""
Create an EventifyLogger record.
Args:
logger_type: 'info' | 'warning' | 'error' | 'critical'
logger_message: str
request: optional HttpRequest (used for IP, user-agent, user if not provided)
user: optional User (overrides request.user)
logger_data: optional str or dict (dict will be JSON-serialized)
"""
try:
resolved_user = user
if resolved_user is None and request and hasattr(request, "user"):
resolved_user = getattr(request.user, "is_authenticated", False) and request.user or None
ip_address = _get_client_ip(request) if request else None
user_agent = _get_user_agent(request) if request else None
if isinstance(logger_data, dict):
logger_data = json.dumps(logger_data)
EventifyLogger.objects.create(
logger_type=logger_type,
logger_message=str(logger_message)[:10000], # cap message length
logger_data=logger_data[:10000] if logger_data else None, # cap data length
logged_user=resolved_user,
logged_ip_address=ip_address,
logged_user_device=None, # defer UA parsing
logged_user_browser=user_agent[:250] if user_agent else None,
)
except Exception:
pass # Never let logging break the app

3
eventify_logger/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
eventify_logger/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

246
events/api.py Normal file
View File

@@ -0,0 +1,246 @@
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 rest_framework.views import APIView
from datetime import datetime
from events.models import Event
from master_data.models import EventType
from mobile_api.utils import validate_token_and_get_user
from eventify_logger.services import log
def _event_to_dict(event, request=None):
"""Serialize Event for JSON."""
data = model_to_dict(
event,
fields=[
"id",
"name",
"description",
"start_date",
"end_date",
"start_time",
"end_time",
"all_year_event",
"latitude",
"longitude",
"pincode",
"district",
"state",
"place",
"venue_name",
"event_status",
"cancelled_reason",
"important_information",
"source",
"created_date",
],
)
# Add event_type info
data["event_type"] = {
"id": event.event_type.id,
"event_type": event.event_type.event_type,
}
if event.event_type.event_type_icon:
if request:
data["event_type"]["event_type_icon"] = request.build_absolute_uri(event.event_type.event_type_icon.url)
else:
data["event_type"]["event_type_icon"] = event.event_type.event_type_icon.url
else:
data["event_type"]["event_type_icon"] = None
return data
@method_decorator(csrf_exempt, name="dispatch")
class EventCreateAPI(APIView):
"""
Create Event API.
Body: token, username (required);
name, description, latitude, longitude, pincode, place, event_type_id (required);
start_date, end_date, start_time, end_time, all_year_event, venue_name,
event_status, cancelled_reason, important_information, source, district, state (optional).
Returns: created event data.
"""
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
# Extract required fields
name = data.get("name")
description = data.get("description")
latitude = data.get("latitude")
longitude = data.get("longitude")
pincode = data.get("pincode")
place = data.get("place")
event_type_id = data.get("event_type_id") or data.get("event_type")
# Validate required fields
if not all([name, description, latitude, longitude, pincode, place, event_type_id]):
return JsonResponse(
{
"status": "error",
"message": "name, description, latitude, longitude, pincode, place, and event_type_id are required.",
},
status=400,
)
# Validate event_type exists
try:
event_type = EventType.objects.get(id=event_type_id)
except EventType.DoesNotExist:
return JsonResponse(
{"status": "error", "message": "EventType not found."},
status=404,
)
# Validate latitude and longitude
try:
latitude = float(latitude)
if latitude < -90 or latitude > 90:
return JsonResponse(
{"status": "error", "message": "latitude must be between -90 and 90."},
status=400,
)
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "latitude must be numeric."},
status=400,
)
try:
longitude = float(longitude)
if longitude < -180 or longitude > 180:
return JsonResponse(
{"status": "error", "message": "longitude must be between -180 and 180."},
status=400,
)
except (TypeError, ValueError):
return JsonResponse(
{"status": "error", "message": "longitude must be numeric."},
status=400,
)
# Handle all_year_event
all_year_event = data.get("all_year_event", False)
if isinstance(all_year_event, str):
all_year_event = all_year_event.lower() in ['true', '1', 'yes', 'on']
# Handle dates and times - clear if all_year_event is True
start_date = None
end_date = None
start_time = None
end_time = None
if not all_year_event:
# Parse start_date
if data.get("start_date"):
try:
start_date = datetime.strptime(data["start_date"], "%Y-%m-%d").date()
except ValueError:
return JsonResponse(
{"status": "error", "message": "Invalid start_date format. Expected YYYY-MM-DD."},
status=400,
)
# Parse end_date
if data.get("end_date"):
try:
end_date = datetime.strptime(data["end_date"], "%Y-%m-%d").date()
except ValueError:
return JsonResponse(
{"status": "error", "message": "Invalid end_date format. Expected YYYY-MM-DD."},
status=400,
)
# Parse start_time
if data.get("start_time"):
try:
start_time = datetime.strptime(data["start_time"], "%H:%M:%S").time()
except ValueError:
try:
start_time = datetime.strptime(data["start_time"], "%H:%M").time()
except ValueError:
return JsonResponse(
{"status": "error", "message": "Invalid start_time format. Expected HH:MM or HH:MM:SS."},
status=400,
)
# Parse end_time
if data.get("end_time"):
try:
end_time = datetime.strptime(data["end_time"], "%H:%M:%S").time()
except ValueError:
try:
end_time = datetime.strptime(data["end_time"], "%H:%M").time()
except ValueError:
return JsonResponse(
{"status": "error", "message": "Invalid end_time format. Expected HH:MM or HH:MM:SS."},
status=400,
)
# Validate event_status if provided
event_status = data.get("event_status", "pending")
valid_statuses = ['created', 'cancelled', 'pending', 'completed', 'postponed']
if event_status not in valid_statuses:
return JsonResponse(
{
"status": "error",
"message": f"Invalid event_status. Must be one of: {', '.join(valid_statuses)}",
},
status=400,
)
# Validate source if provided
source = data.get("source", "official")
valid_sources = ['official', 'community']
if source not in valid_sources:
return JsonResponse(
{
"status": "error",
"message": f"Invalid source. Must be one of: {', '.join(valid_sources)}",
},
status=400,
)
# Create event
event = Event.objects.create(
name=name,
description=description,
start_date=start_date,
end_date=end_date,
start_time=start_time,
end_time=end_time,
all_year_event=all_year_event,
latitude=latitude,
longitude=longitude,
pincode=pincode,
district=data.get("district", ""),
state=data.get("state", ""),
place=place,
venue_name=data.get("venue_name", ""),
event_type=event_type,
event_status=event_status,
cancelled_reason=data.get("cancelled_reason", "NA"),
important_information=data.get("important_information", ""),
source=source,
)
log("info", "Event created", request=request, user=user, logger_data={"event_id": event.id, "event_name": name})
return JsonResponse(
{
"status": "success",
"message": "Event created successfully.",
"event": _event_to_dict(event, request),
},
status=201,
)
except Exception as e:
log("error", "Event create exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)

View File

@@ -0,0 +1,33 @@
# Generated by Django 4.2.27 on 2026-03-13 16:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0006_alter_event_source'),
]
operations = [
migrations.AddField(
model_name='event',
name='gst_percentage_1',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='event',
name='gst_percentage_2',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='event',
name='include_gst',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='event',
name='event_status',
field=models.CharField(choices=[('created', 'Created'), ('cancelled', 'Cancelled'), ('pending', 'Pending'), ('completed', 'Completed'), ('postponed', 'Postponed'), ('published', 'Published'), ('live', 'Live'), ('flagged', 'Flagged')], default='pending', max_length=250),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.2.27 on 2026-03-14 15:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('partner', '0001_initial'),
('events', '0007_event_gst_percentage_1_event_gst_percentage_2_and_more'),
]
operations = [
migrations.AddField(
model_name='event',
name='is_partner_event',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='event',
name='partner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partner.partner'),
),
]

View File

@@ -1,6 +1,7 @@
from random import choices
from django.db import models
from master_data.models import EventType
from partner.models import Partner
class Event(models.Model):
@@ -21,10 +22,16 @@ class Event(models.Model):
place = models.CharField(max_length=200)
is_bookable = models.BooleanField(default=False)
include_gst = models.BooleanField(default=False)
gst_percentage_1 = models.IntegerField(default=0)
gst_percentage_2 = models.IntegerField(default=0)
is_eventify_event = models.BooleanField(default=True)
outside_event_url = models.URLField(default='NA')
is_partner_event = models.BooleanField(default=False)
partner = models.ForeignKey(Partner, on_delete=models.CASCADE, blank=True, null=True)
event_type = models.ForeignKey(EventType, on_delete=models.CASCADE)
event_status = models.CharField(max_length=250, choices=[
('created', 'Created'),
@@ -32,6 +39,9 @@ class Event(models.Model):
('pending', 'Pending'),
('completed', 'Completed'),
('postponed', 'Postponed'),
('published', 'Published'),
('live', 'Live'),
('flagged', 'Flagged'),
], default='pending')
cancelled_reason = models.TextField(default='NA')

View File

@@ -1,5 +1,6 @@
from django.urls import path
from . import views
from . import api
app_name = 'events'
@@ -14,3 +15,8 @@ urlpatterns = [
path('<int:pk>/images/<int:img_id>/delete/', views.delete_event_image, name='delete_event_image'),
path('<int:pk>/images/<int:img_id>/primary/', views.set_primary_image, name='set_primary_image'),
]
# Event API URLs
urlpatterns += [
path('api/create/', api.EventCreateAPI.as_view(), name='event_api_create'),
]

View File

@@ -10,8 +10,6 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from .models import Event
class EventListView(LoginRequiredMixin, generic.ListView):

View File

@@ -2,7 +2,7 @@ from django.urls import path
from .views import *
# User URLS
# Customer URLS
urlpatterns = [
path('user/register/', RegisterView.as_view(), name='json_register'),
path('user/login/', LoginView.as_view(), name='json_login'),

View File

@@ -11,6 +11,7 @@ from django.contrib.auth import logout
from mobile_api.utils import validate_token_and_get_user
from utils.errors_json_convertor import simplify_form_errors
from accounts.models import User
from eventify_logger.services import log
@method_decorator(csrf_exempt, name='dispatch')
@@ -22,9 +23,12 @@ class RegisterView(View):
if form.is_valid():
user = form.save()
token, _ = Token.objects.get_or_create(user=user)
log("info", "API user registration", request=request, user=user)
return JsonResponse({'message': 'User registered successfully', 'token': token.key}, status=201)
log("warning", "API registration failed", request=request, logger_data=dict(errors=form.errors))
return JsonResponse({'errors': form.errors}, status=400)
except Exception as e:
log("error", "API registration exception", request=request, logger_data={"error": str(e)})
return JsonResponse({'error': str(e)}, status=500)
@@ -47,6 +51,7 @@ class WebRegisterView(View):
user = form.save()
token, _ = Token.objects.get_or_create(user=user)
print('3')
log("info", "Web user registration", request=request, user=user)
response = {
'message': 'User registered successfully',
'token': token.key,
@@ -55,8 +60,10 @@ class WebRegisterView(View):
'phone_number': user.phone_number,
}
return JsonResponse(response, status=201)
log("warning", "Web registration failed", request=request, logger_data=dict(errors=form.errors))
return JsonResponse({'errors': form.errors}, status=400)
except Exception as e:
log("error", "Web registration exception", request=request, logger_data={"error": str(e)})
return JsonResponse({'error': str(e)}, status=500)
@@ -73,6 +80,7 @@ class LoginView(View):
user = form.cleaned_data['user']
token, _ = Token.objects.get_or_create(user=user)
print('3')
log("info", "API login", request=request, user=user)
response = {
'message': 'Login successful',
'token': token.key,
@@ -95,8 +103,10 @@ class LoginView(View):
print(response)
return JsonResponse(response, status=200)
log("warning", "API login failed", request=request, logger_data=dict(errors=form.errors))
return JsonResponse(simplify_form_errors(form), status=401)
except Exception as e:
log("error", "API login exception", request=request, logger_data={"error": str(e)})
return JsonResponse({'error': str(e)}, status=500)
@@ -126,6 +136,7 @@ class LogoutView(View):
if error_response:
return error_response
log("info", "API logout", request=request, user=user)
# 🔍 Call Django's built-in logout
logout(request)
@@ -138,6 +149,7 @@ class LogoutView(View):
})
except Exception as e:
log("error", "API logout exception", request=request, logger_data={"error": str(e)})
return JsonResponse({"status": "error", "message": str(e)}, status=500)

0
partner/__init__.py Normal file
View File

24
partner/admin.py Normal file
View File

@@ -0,0 +1,24 @@
from django.contrib import admin
from .models import Partner
@admin.register(Partner)
class PartnerAdmin(admin.ModelAdmin):
list_display = ('name', 'partner_type', 'primary_contact_person_name', 'primary_contact_person_email', 'is_kyc_compliant', 'kyc_compliance_status')
list_filter = ('partner_type', 'is_kyc_compliant', 'kyc_compliance_status')
search_fields = ('name', 'primary_contact_person_name', 'primary_contact_person_email', 'primary_contact_person_phone')
fieldsets = (
('Basic Information', {
'fields': ('name', 'partner_type')
}),
('Contact Information', {
'fields': ('primary_contact_person_name', 'primary_contact_person_email', 'primary_contact_person_phone', 'website_url')
}),
('Address', {
'fields': ('address', 'city', 'state', 'country', 'pincode', 'latitude', 'longitude')
}),
('KYC Compliance', {
'fields': ('is_kyc_compliant', 'kyc_compliance_status', 'kyc_compliance_reason', 'kyc_compliance_document_type',
'kyc_compliance_document_other_type', 'kyc_compliance_document_file', 'kyc_compliance_document_number')
}),
)

1118
partner/api.py Normal file

File diff suppressed because it is too large Load Diff

6
partner/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class PartnerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'partner'

View File

@@ -0,0 +1,41 @@
# Generated by Django 4.2.27 on 2026-03-13 16:55
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Partner',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=250)),
('partner_type', models.CharField(choices=[('venue', 'Venue'), ('promoter', 'Promoter'), ('sponsor', 'Sponsor'), ('vendor', 'Vendor'), ('affiliate', 'Affiliate'), ('other', 'Other')], max_length=250)),
('primary_contact_person_name', models.CharField(max_length=250)),
('primary_contact_person_email', models.EmailField(max_length=254)),
('primary_contact_person_phone', models.CharField(max_length=15)),
('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('suspended', 'Suspended'), ('deleted', 'Deleted'), ('archived', 'Archived')], default='active', max_length=250)),
('address', models.TextField(blank=True, null=True)),
('city', models.CharField(blank=True, max_length=250, null=True)),
('state', models.CharField(blank=True, max_length=250, null=True)),
('country', models.CharField(blank=True, max_length=250, null=True)),
('website_url', models.URLField(blank=True, null=True)),
('pincode', models.CharField(blank=True, max_length=10, null=True)),
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('is_kyc_compliant', models.BooleanField(default=False)),
('kyc_compliance_status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('high_risk', 'High Risk'), ('low_risk', 'Low Risk'), ('medium_risk', 'Medium Risk')], default='pending', max_length=250)),
('kyc_compliance_reason', models.TextField(blank=True, null=True)),
('kyc_compliance_document_type', models.CharField(blank=True, choices=[('aadhaar', 'Aadhaar'), ('pan', 'PAN'), ('driving_license', 'Driving License'), ('voter_id', 'Voter ID'), ('passport', 'Passport'), ('other', 'Other')], max_length=250, null=True)),
('kyc_compliance_document_other_type', models.CharField(blank=True, max_length=250, null=True)),
('kyc_compliance_document_file', models.FileField(blank=True, null=True, upload_to='kyc_documents/')),
('kyc_compliance_document_number', models.CharField(blank=True, max_length=250, null=True)),
],
),
]

View File

69
partner/models.py Normal file
View File

@@ -0,0 +1,69 @@
from django.db import models
PARTNER_TYPE_CHOICES = (
('venue', 'Venue'),
('promoter', 'Promoter'),
('sponsor', 'Sponsor'),
('vendor', 'Vendor'),
('affiliate', 'Affiliate'),
('other', 'Other'),
)
KYC_DOCUMENT_TYPE_CHOICES = (
('aadhaar', 'Aadhaar'),
('pan', 'PAN'),
('driving_license', 'Driving License'),
('voter_id', 'Voter ID'),
('passport', 'Passport'),
('other', 'Other'),
)
KYC_COMPLIANCE_STATUS_CHOICES = (
('pending', 'Pending'),
('approved', 'Approved'),
('rejected', 'Rejected'),
('high_risk', 'High Risk'),
('low_risk', 'Low Risk'),
('medium_risk', 'Medium Risk'),
)
STATUS_CHOICES = (
('active', 'Active'),
('inactive', 'Inactive'),
('pending', 'Pending'),
('suspended', 'Suspended'),
('deleted', 'Deleted'),
('archived', 'Archived'),
)
class Partner(models.Model):
name = models.CharField(max_length=250)
partner_type = models.CharField(max_length=250, choices=PARTNER_TYPE_CHOICES)
primary_contact_person_name = models.CharField(max_length=250)
primary_contact_person_email = models.EmailField()
primary_contact_person_phone = models.CharField(max_length=15)
status = models.CharField(max_length=250, choices=STATUS_CHOICES, default='active')
address = models.TextField(blank=True, null=True)
city = models.CharField(max_length=250, blank=True, null=True)
state = models.CharField(max_length=250, blank=True, null=True)
country = models.CharField(max_length=250, blank=True, null=True)
website_url = models.URLField(blank=True, null=True)
pincode = models.CharField(max_length=10, blank=True, null=True)
latitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True)
is_kyc_compliant = models.BooleanField(default=False)
kyc_compliance_status = models.CharField(max_length=250, choices=KYC_COMPLIANCE_STATUS_CHOICES, default='pending')
kyc_compliance_reason = models.TextField(blank=True, null=True)
kyc_compliance_document_type = models.CharField(max_length=250, choices=KYC_DOCUMENT_TYPE_CHOICES, blank=True, null=True)
kyc_compliance_document_other_type = models.CharField(max_length=250, blank=True, null=True)
kyc_compliance_document_file = models.FileField(upload_to='kyc_documents/', blank=True, null=True)
kyc_compliance_document_number = models.CharField(max_length=250, blank=True, null=True)
def __str__(self):
return self.name

3
partner/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

40
partner/urls.py Normal file
View File

@@ -0,0 +1,40 @@
from django.urls import path
from partner.api import (
PartnerCreateAPI,
PartnerListAPI,
PartnerUpdateAPI,
PartnerDeleteAPI,
PartnerUpdateKYCDocumentsAPI,
PartnerUpdateAddressLocationAPI,
PartnerCreateUserAPI,
PartnerListUsersAPI,
PartnerUpdateUserAPI,
PartnerDeleteUserAPI,
PartnerListAllAPI,
PartnerListKYCCompliantAPI,
PartnerListKYCPendingAPI,
PartnerListHighRiskAPI,
PartnerListJoinedThisWeekAPI,
)
urlpatterns = [
path("create/", PartnerCreateAPI.as_view(), name="partner_create"),
path("list/", PartnerListAPI.as_view(), name="partner_list"),
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"),
path("update-address-location/", PartnerUpdateAddressLocationAPI.as_view(), name="partner_update_address_location"),
path("create-user/", PartnerCreateUserAPI.as_view(), name="partner_create_user"),
path("list-users/", PartnerListUsersAPI.as_view(), name="partner_list_users"),
path("update-user/", PartnerUpdateUserAPI.as_view(), name="partner_update_user"),
path("delete-user/", PartnerDeleteUserAPI.as_view(), name="partner_delete_user"),
# Partner List APIs (Filtered by Criteria)
path("list-all/", PartnerListAllAPI.as_view(), name="partner_list_all"),
path("list-kyc-compliant/", PartnerListKYCCompliantAPI.as_view(), name="partner_list_kyc_compliant"),
path("list-kyc-pending/", PartnerListKYCPendingAPI.as_view(), name="partner_list_kyc_pending"),
path("list-high-risk/", PartnerListHighRiskAPI.as_view(), name="partner_list_high_risk"),
path("list-joined-this-week/", PartnerListJoinedThisWeekAPI.as_view(), name="partner_list_joined_this_week"),
]

3
partner/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Eventify Partner Portal</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'accounts:partner_dashboard' %}">Eventify Partner Portal</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navmenu">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navmenu">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:partner_dashboard' %}">Dashboard</a>
</li>
{% if user.role == "partner" or user.role == "partner_manager" %}
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:partner_user_list' %}">Users</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
{% if user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="#">
{% if user.first_name and user.last_name %}
{{ user.first_name }} {{ user.last_name }}
{% elif user.username %}
{{ user.username }}
{% else %}
{{ user.email }}
{% endif %}
</a></li>
<li class="nav-item"><a class="nav-link text-danger" href="{% url 'accounts:partner_logout' %}">Logout</a></li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{% url 'accounts:partner_login' %}">Login</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
{% extends 'partner/base.html' %}
{% block content %}
<div class="row text-center">
<div class="col-md-4">
<div class="card p-3 shadow-sm">
<h5>Total Partner Users</h5>
<h2>{{ total_partner_users }}</h2>
</div>
</div>
<!-- Add more partner-specific statistics here -->
</div>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Partner Login</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<div class="container d-flex justify-content-center mt-5">
<div class="col-md-4">
<div class="card shadow-sm p-4">
<h2 class="text-center mb-3">Eventify Partner Portal</h2>
<h4 class="text-center mb-3">Login</h4>
{% if messages %}
{% for message in messages %}
<div class="alert alert-danger py-2">{{ message }}</div>
{% endfor %}
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
<div class="mb-3">
{{ form.username.label_tag }}
{{ form.username }}
{% for error in form.username.errors %}
<small class="text-danger">{{ error }}</small>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label_tag }}
{{ form.password }}
{% for error in form.password.errors %}
<small class="text-danger">{{ error }}</small>
{% endfor %}
</div>
<button class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
{% extends 'partner/base.html' %}
{% block content %}
<h3>Delete Partner User</h3>
<p>Are you sure you want to delete <strong>{{ object.username }}</strong>?</p>
<form method="post">{% csrf_token %}<button class="btn btn-danger">Yes, delete</button>
<a class="btn btn-secondary" href="{% url 'accounts:partner_user_list' %}">Cancel</a></form>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends 'partner/base.html' %}
{% block content %}
<div class="container mt-4">
<h3>{% if object %}Edit{% else %}Add{% endif %} Partner User</h3>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button class="btn btn-primary">Save</button>
<a class="btn btn-secondary" href="{% url 'accounts:partner_user_list' %}">Cancel</a>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'partner/base.html' %}
{% block content %}
<div class="d-flex justify-content-between mb-3">
<h3>Partner Users</h3>
<a class="btn btn-success" href="{% url 'accounts:partner_user_add' %}">Add User</a>
</div>
<table class="table table-striped">
<thead><tr><th>#</th><th>Username</th><th>Email</th><th>Phone</th><th>Role</th><th>Actions</th></tr></thead>
<tbody>
{% for u in users %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ u.username }}</td>
<td>{{ u.email }}</td>
<td>{{ u.phone_number }}</td>
<td>{{ u.get_role_display }}</td>
<td>
<a class="btn btn-sm btn-primary" href="{% url 'accounts:partner_user_edit' u.pk %}">Edit</a>
<a class="btn btn-sm btn-danger" href="{% url 'accounts:partner_user_delete' u.pk %}">Delete</a>
</td>
</tr>
{% empty %}
<tr><td colspan="6">No users yet.</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

98
urls_text.txt Normal file
View File

@@ -0,0 +1,98 @@
/admin/
/
/register/
/logout/
/dashboard/
/users/
/users/add/
/users/<int:pk>/edit/
/users/<int:pk>/delete/
/master-data/event-types/
/master-data/event-types/add/
/master-data/event-types/<int:pk>/edit/
/master-data/event-types/<int:pk>/delete/
/events/
/events/add/
/events/<int:pk>/edit/
/events/<int:pk>/delete/
/events/<int:pk>/images/
/events/<int:pk>/images/add/
/events/<int:pk>/images/<int:img_id>/delete/
/events/<int:pk>/images/<int:img_id>/primary/
/events/api/create/
/accounts/login/
/accounts/logout/
/accounts/dashboard/
/accounts/users/
/accounts/accounts/users/add/
/accounts/users/<int:pk>/edit/
/accounts/users/<int:pk>/delete/
/accounts/api/login/
/accounts/api/logout/
/accounts/api/users/list/
/accounts/api/users/create/
/accounts/api/users/update/
/accounts/api/users/delete/
/accounts/api/partner/login/
/accounts/api/partner/logout/
/accounts/api/partner/dashboard/
/accounts/api/partner/users/list/
/accounts/api/partner/users/create/
/accounts/api/partner/users/update/
/accounts/api/partner/users/delete/
/bookings/ticket-meta/create/
/bookings/ticket-meta/list/
/bookings/ticket-meta/update/
/bookings/ticket-meta/delete/
/bookings/ticket-meta/deactivate/
/bookings/ticket-type/create/
/bookings/ticket-type/list/
/bookings/ticket-type/update/
/bookings/ticket-type/delete/
/bookings/ticket-type/deactivate/
/bookings/cart/add/
/bookings/cart/delete/
/bookings/checkout/
/bookings/check-in/
/partner/create/
/partner/list/
/partner/update/
/partner/delete/
/partner/update-kyc-documents/
/partner/update-address-location/
/partner/create-user/
/partner/list-users/
/partner/update-user/
/partner/delete-user/
/partner/list-all/
/partner/list-kyc-compliant/
/partner/list-kyc-pending/
/partner/list-high-risk/
/partner/list-joined-this-week/
/banking/payment-gateway/create/
/banking/payment-gateway/list/
/banking/payment-gateway/update/
/banking/payment-gateway/delete/
/banking/payment-gateway-credentials/create/
/banking/payment-gateway-credentials/list/
/banking/payment-gateway-credentials/update/
/banking/payment-gateway-credentials/delete/
/api/user/register/
/api/user/login/
/api/user/status/
/api/user/logout/
/api/user/update-profile/
/api/events/type-list/
/api/events/pincode-events/
/api/events/event-details/
/api/events/event-images/
/api/events/events-by-category/
/api/events/events-by-month-year/
/api/events/events-by-date/