import logging from decimal import Decimal, InvalidOperation from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseBadRequest from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone from datetime import datetime from django.utils.text import slugify from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from events.models import Event, TicketOrder from events.services import ( EventsError, create_checkout_session, create_ticket_order, mark_paid, ) logger = logging.getLogger(__name__) def _site_base(request) -> str: base = (settings.PUBLIC_SITE_URL or "").rstrip("/") if base: return base return request.build_absolute_uri("/").rstrip("/") def event_list(request): events = Event.objects.filter(is_published=True, starts_at__gte=timezone.now()) return render(request, "events/list.html", {"events": events}) def event_detail(request, slug): event = get_object_or_404(Event, slug=slug, is_published=True) return render(request, "events/detail.html", {"event": event}) @require_http_methods(["GET", "POST"]) def buy_tickets(request, slug): event = get_object_or_404(Event, slug=slug, is_published=True) if request.method == "POST": email = (request.POST.get("email") or "").strip() name = (request.POST.get("customer_name") or "").strip() try: quantity = int(request.POST.get("quantity") or "1") except ValueError: quantity = 0 try: order = create_ticket_order( event, email=email, customer_name=name, quantity=quantity ) base = _site_base(request) success = base + reverse( "events:checkout_success", kwargs={"pk": order.pk} ) cancel = base + reverse("events:checkout_cancel", kwargs={"pk": order.pk}) url = create_checkout_session( order, success_url=success + "?session_id={CHECKOUT_SESSION_ID}", cancel_url=cancel, ) except EventsError as exc: messages.error(request, str(exc)) except Exception as exc: # noqa: BLE001 logger.exception("ticket checkout failed") messages.error(request, f"Could not start checkout: {exc}") else: return redirect(url) return render(request, "events/buy.html", {"event": event}) def checkout_success(request, pk): order = get_object_or_404(TicketOrder.objects.select_related("event"), pk=pk) return render(request, "events/success.html", {"order": order}) def checkout_cancel(request, pk): order = get_object_or_404(TicketOrder.objects.select_related("event"), pk=pk) return render(request, "events/cancel.html", {"order": order}) @login_required def portal_event_list(request): events = Event.objects.all().order_by("-starts_at") return render(request, "events/portal/list.html", {"events": events}) @login_required @require_http_methods(["GET", "POST"]) def portal_event_edit(request, pk=None): event = get_object_or_404(Event, pk=pk) if pk else None if request.method == "POST": title = (request.POST.get("title") or "").strip() description = (request.POST.get("description") or "").strip() venue = (request.POST.get("venue") or "").strip() slug = (request.POST.get("slug") or "").strip() starts_raw = (request.POST.get("starts_at") or "").strip() errors = [] if not title: errors.append("Title is required.") starts_at = None if starts_raw: try: starts_at = datetime.fromisoformat(starts_raw) except ValueError: starts_at = None if starts_at is None: errors.append("Start date/time is required.") elif timezone.is_naive(starts_at): starts_at = timezone.make_aware(starts_at, timezone.get_current_timezone()) try: capacity = int(request.POST.get("capacity") or "0") except ValueError: capacity = 0 errors.append("Capacity must be a number.") try: price = Decimal(request.POST.get("price") or "0") if price < 0: raise InvalidOperation except Exception: price = None errors.append("Enter a valid price.") if errors: for err in errors: messages.error(request, err) else: if event is None: event = Event() event.title = title event.description = description event.venue = venue event.slug = slugify(slug)[:220] if slug else "" event.starts_at = starts_at event.capacity = max(capacity, 0) event.price = price event.currency = (settings.STRIPE_CURRENCY or "usd").lower() event.is_published = request.POST.get("is_published") == "on" event.save() messages.success(request, f"Saved {event.title}.") return redirect("events_portal:event_list") return render(request, "events/portal/edit.html", {"event": event}) @login_required def portal_event_detail(request, pk): event = get_object_or_404(Event, pk=pk) orders = event.ticket_orders.select_related().prefetch_related("tickets") return render( request, "events/portal/detail.html", {"event": event, "orders": orders}, ) @csrf_exempt @require_http_methods(["POST"]) def stripe_webhook(request): secret = (settings.STRIPE_WEBHOOK_SECRET or "").strip() if not secret: logger.error("STRIPE_WEBHOOK_SECRET unset") return HttpResponseBadRequest("webhook not configured") try: import stripe except ImportError: return HttpResponseBadRequest("stripe not installed") sig = request.headers.get("Stripe-Signature", "") try: event = stripe.Webhook.construct_event(request.body, sig, secret) except Exception: logger.exception("events stripe webhook signature failed") return HttpResponseBadRequest("invalid signature") obj = event.get("data", {}).get("object", {}) or {} if event.get("type") != "checkout.session.completed": return HttpResponse("ok") order_id = (obj.get("metadata") or {}).get("ticket_order_id") or "" order = None if order_id: order = TicketOrder.objects.filter(pk=order_id).first() if order is None: session_id = obj.get("id") or "" order = TicketOrder.objects.filter( stripe_checkout_session_id=session_id ).first() if order and order.status != TicketOrder.Status.PAID: mark_paid(order, stripe_id=obj.get("id") or "") logger.info("ticket order %s marked paid", order.number) return HttpResponse("ok")