370 lines
14 KiB
Python
370 lines
14 KiB
Python
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)
|