TheDeepDas's picture
Fix Django backend deployment on HF Spaces
c68b343
from django.shortcuts import get_object_or_404
from django.db import transaction
from rest_framework.exceptions import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.http import JsonResponse
from django.core.mail import send_mail
from django.conf import settings
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import api_view, permission_classes
import razorpay
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.db.models import F
from apps.orders.models import PaymentOrder
from .models import (
Booking,
StudioBooking,
UpcomingEvent,
Workshop,
WorkshopSlot,
WorkshopRegistration,
Experience,
ExperienceSlot,
)
from .serializers import (
BookingSerializer,
StudioBookingSerializer,
UpcomingEventSerializer,
WorkshopSerializer,
WorkshopSlotSerializer,
WorkshopRegistrationSerializer,
ExperienceSlotSerializer,
ExperienceSerializer, # ✅ ADD THIS
)
razorpay_client = razorpay.Client(
auth=(settings.RAZORPAY_KEY_ID, settings.RAZORPAY_KEY_SECRET)
)
# =========================
# EXPERIENCE BOOKING (PAYMENT FIRST)
# =========================
class CreateBookingView(APIView):
def post(self, request):
serializer = BookingSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
slot = serializer.validated_data["slot"]
people = serializer.validated_data["number_of_people"]
experience = serializer.validated_data["experience"]
# 🔒 ATOMIC BLOCK (prevents race conditions)
with transaction.atomic():
slot = ExperienceSlot.objects.select_for_update().get(id=slot.id)
available = slot.total_slots - slot.booked_slots
if people > available:
raise ValidationError({
"slot": f"Only {available} slots left for this time slot."
})
booking = serializer.save(
status="pending",
payment_amount=experience.price
)
# ✅ Reserve seats
slot.booked_slots += people
slot.save()
# ✅ CREATE RAZORPAY ORDER
razorpay_order = razorpay_client.order.create({
"amount": booking.payment_amount * 100,
"currency": "INR",
"payment_capture": 1
})
payment_order = PaymentOrder.objects.create(
user=request.user if request.user.is_authenticated else None,
order_type="EXPERIENCE",
linked_object_id=booking.id,
linked_app="experiences",
amount=booking.payment_amount,
razorpay_order_id=razorpay_order["id"],
)
booking.payment_order = payment_order
booking.save()
return Response({
"booking_id": booking.id,
"razorpay_order_id": payment_order.razorpay_order_id,
"amount": payment_order.amount,
}, status=status.HTTP_201_CREATED)
razorpay_client = razorpay.Client(
auth=(settings.RAZORPAY_KEY_ID, settings.RAZORPAY_KEY_SECRET)
)
class ReleaseExperienceSlotView(APIView):
def post(self, request):
booking_id = request.data.get("booking_id")
if not booking_id:
return Response(
{"error": "booking_id required"},
status=status.HTTP_400_BAD_REQUEST
)
try:
with transaction.atomic():
booking = Booking.objects.select_for_update().get(id=booking_id)
# Only release if not confirmed
if booking.status != "pending":
return Response(
{"message": "Booking already processed"},
status=status.HTTP_200_OK
)
slot = booking.slot
slot.booked_slots -= booking.number_of_people
slot.save()
booking.status = "failed"
booking.save()
return Response(
{"message": "Slot released successfully"},
status=status.HTTP_200_OK
)
except Booking.DoesNotExist:
return Response(
{"error": "Booking not found"},
status=status.HTTP_404_NOT_FOUND
)
class VerifyExperiencePaymentView(APIView):
def post(self, request):
data = request.data
try:
# 1️⃣ Verify signature
razorpay_client.utility.verify_payment_signature({
"razorpay_order_id": data["razorpay_order_id"],
"razorpay_payment_id": data["razorpay_payment_id"],
"razorpay_signature": data["razorpay_signature"],
})
# 2️⃣ Fetch payment order
payment_order = PaymentOrder.objects.get(
razorpay_order_id=data["razorpay_order_id"]
)
# 3️⃣ Mark payment as paid
payment_order.status = "PAID"
payment_order.razorpay_payment_id = data["razorpay_payment_id"]
payment_order.save()
# 4️⃣ Confirm booking
booking = Booking.objects.get(id=payment_order.linked_object_id)
booking.status = "confirmed"
booking.save()
# 🔍 TEMP DEBUG — add just above send_mail
print("Experience fields:")
print(booking.experience._meta.get_fields())
# 5️⃣ Send email
if booking.email:
send_mail(
subject="Your Experience Booking is Confirmed 🎉",
message=f"""
Hi {booking.full_name},
Your experience booking has been successfully confirmed!
📅 Date: {booking.booking_date}
🎨 Experience: {booking.experience.title}
💰 Amount Paid: ₹{booking.payment_amount}
We look forward to welcoming you ✨
– Team Basho
""",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[booking.email],
fail_silently=False,
)
return Response({
"message": "Successfully placed order / booked experience"
}, status=status.HTTP_200_OK)
except razorpay.errors.SignatureVerificationError:
return Response(
{"error": "Payment verification failed"},
status=status.HTTP_400_BAD_REQUEST
)
# =========================
# STUDIO BOOKING (NO PAYMENT)
# =========================
@method_decorator(csrf_exempt, name="dispatch")
class CreateStudioBookingView(APIView):
def post(self, request):
serializer = StudioBookingSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
booking = serializer.save()
try:
send_mail(
subject="Your Studio Visit is Confirmed ✨",
message=(
f"Hi {booking.full_name},\n\n"
f"Your studio visit has been confirmed.\n\n"
f"Date: {booking.visit_date}\n"
f"Time Slot: {booking.time_slot}\n\n"
f"– Basho Studio"
),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[booking.email],
fail_silently=True, # 🔥 THIS IS THE KEY
)
except Exception as e:
print("⚠️ Studio email failed:", str(e))
return Response(
{"message": "Studio booking confirmed"},
status=status.HTTP_201_CREATED
)
# =========================
# LISTING VIEWS
# =========================
class ListUpcomingEventsView(APIView):
def get(self, request):
events = UpcomingEvent.objects.all()
serializer = UpcomingEventSerializer(events, many=True)
return Response(serializer.data)
class ListWorkshopsView(APIView):
def get(self, request):
workshops = Workshop.objects.filter(is_active=True)
serializer = WorkshopSerializer(workshops, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class WorkshopDetailView(APIView):
def get(self, request, workshop_id):
workshop = get_object_or_404(Workshop, id=workshop_id, is_active=True)
serializer = WorkshopSerializer(workshop)
return Response(serializer.data, status=status.HTTP_200_OK)
class ListWorkshopSlotsView(APIView):
def get(self, request, workshop_id):
workshop = get_object_or_404(Workshop, id=workshop_id, is_active=True)
slots = WorkshopSlot.objects.filter(
workshop=workshop,
is_available=True
).order_by("date", "start_time")
serializer = WorkshopSlotSerializer(slots, many=True)
return Response(serializer.data)
class ListExperienceSlotsView(APIView):
def get(self, request, experience_id):
experience = get_object_or_404(Experience, id=experience_id, is_active=True)
slots = ExperienceSlot.objects.filter(
experience=experience,
is_active=True
).order_by("date", "start_time")
serializer = ExperienceSlotSerializer(slots, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class ListExperiencesView(APIView):
def get(self, request):
experiences = Experience.objects.filter(is_active=True)
serializer = ExperienceSerializer(experiences, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class ListExperienceAvailableDatesView(APIView):
def get(self, request, experience_id):
experience = get_object_or_404(
Experience,
id=experience_id,
is_active=True
)
# Slots that still have availability
slots = (
ExperienceSlot.objects
.filter(
experience=experience,
is_active=True,
total_slots__gt=F("booked_slots")
)
.values_list("date", flat=True)
.distinct()
.order_by("date")
)
# Convert dates to string (frontend-friendly)
dates = [d.isoformat() for d in slots]
return Response(dates, status=status.HTTP_200_OK)
class ListExperienceSlotsByDateView(APIView):
def get(self, request, experience_id):
date = request.query_params.get("date")
if not date:
return Response(
{"error": "date query param is required"},
status=status.HTTP_400_BAD_REQUEST
)
experience = get_object_or_404(
Experience,
id=experience_id,
is_active=True
)
slots = (
ExperienceSlot.objects
.filter(
experience=experience,
date=date,
is_active=True
)
.order_by("start_time")
)
serializer = ExperienceSlotSerializer(slots, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
# =========================
# WORKSHOP REGISTRATION (PAYMENT FIRST)
# =========================
class CreateWorkshopRegistrationView(APIView):
def post(self, request):
serializer = WorkshopRegistrationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
registration = serializer.save(status="pending")
amount = (
registration.workshop.price * registration.number_of_participants
if registration.workshop.price_per_person
else registration.workshop.price
)
razorpay_order = razorpay_client.order.create({
"amount": amount * 100, # paise
"currency": "INR",
"payment_capture": 1
})
payment_order = PaymentOrder.objects.create(
user=request.user if request.user.is_authenticated else None,
order_type="WORKSHOP",
linked_object_id=registration.id,
linked_app="experiences",
amount=amount,
razorpay_order_id=razorpay_order["id"],
)
registration.payment_order = payment_order
registration.save()
return Response(
{
"registration_id": registration.id,
"razorpay_order_id": payment_order.razorpay_order_id,
"amount": amount,
},
status=status.HTTP_201_CREATED
)
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def my_workshops(request):
orders = PaymentOrder.objects.filter(
user=request.user,
order_type="WORKSHOP",
status="PAID"
).order_by("-created_at")
data = [{
"id": o.id,
"amount": o.amount,
"date": o.created_at,
"linked_object_id": o.linked_object_id
} for o in orders]
return JsonResponse({"workshops": data})
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def my_experiences(request):
orders = PaymentOrder.objects.filter(
user=request.user,
order_type="EXPERIENCE",
status="PAID"
).order_by("-created_at")
data = [{
"id": o.id,
"amount": o.amount,
"date": o.created_at,
"linked_object_id": o.linked_object_id
} for o in orders]
return JsonResponse({"experiences": data})