Files
eventify_backend/bookings/tickets_view/booking_api.py

370 lines
14 KiB
Python
Raw Normal View History

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)