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)