import logging import stripe from django.conf import settings from rest_framework import permissions, status from rest_framework.response import Response from rest_framework.views import APIView from finance.models import Invoice, Payment, SubscriptionPlan, UserSubscription from finance.serializers import ( CheckoutSessionSerializer, InvoiceSerializer, PaymentSerializer, PortalSessionSerializer, SubscriptionPlanSerializer, ) from finance.services.plans import ( needs_checkout, plan_to_dict, seed_subscription_plans, ) from finance.services.quotas import get_usage_snapshot from finance.services.stripe_service import ( StripeNotConfiguredError, create_billing_portal_session, create_checkout_session, resolve_stripe_customer_id, ) from finance.services.webhooks import dispatch_stripe_event logger = logging.getLogger(__name__) class CreateCheckoutSessionView(APIView): """Create a Stripe Checkout Session and return the hosted redirect URL.""" def post(self, request): serializer = CheckoutSessionSerializer(data=request.data) serializer.is_valid(raise_exception=True) try: sub = request.user.subscription except UserSubscription.DoesNotExist: sub = None if ( sub is not None and sub.is_active and sub.source == UserSubscription.Source.BACKER ): return Response( { "detail": ( "This account has complimentary Backer access and " "does not require payment." ), "needs_checkout": False, }, status=status.HTTP_400_BAD_REQUEST, ) try: session, plan = create_checkout_session( user=request.user, success_url=serializer.validated_data.get("success_url"), cancel_url=serializer.validated_data.get("cancel_url"), plan_slug=serializer.validated_data.get("plan_slug"), ) except ValueError as exc: return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) except StripeNotConfiguredError as exc: return Response( {"detail": str(exc)}, status=status.HTTP_503_SERVICE_UNAVAILABLE, ) except stripe.StripeError as exc: logger.exception("Stripe Checkout Session creation failed") return Response( {"detail": str(getattr(exc, "user_message", None) or exc)}, status=status.HTTP_502_BAD_GATEWAY, ) # Persist a draft invoice keyed by checkout session for admin visibility # before the webhook fires. Invoice.objects.update_or_create( stripe_checkout_session_id=session.id, defaults={ "user": request.user, "company": request.user.company, "provider": Invoice.Provider.STRIPE, "status": Invoice.Status.OPEN, "currency": plan.currency or settings.SUBSCRIPTION_PRICE_CURRENCY, "amount_due": plan.price_cents, "amount_paid": 0, "stripe_customer_id": getattr(session, "customer", None) or "", "description": plan.name, }, ) return Response( { "checkout_url": session.url, "session_id": session.id, "plan_slug": plan.slug, }, status=status.HTTP_201_CREATED, ) class CreateBillingPortalSessionView(APIView): """Create a Stripe Customer Portal session and return the hosted URL.""" def post(self, request): serializer = PortalSessionSerializer(data=request.data) serializer.is_valid(raise_exception=True) customer_id = resolve_stripe_customer_id(user=request.user) if not customer_id: return Response( { "detail": ( "No Stripe customer found for this account. " "Complete Checkout first to manage billing." ) }, status=status.HTTP_400_BAD_REQUEST, ) try: session = create_billing_portal_session( customer_id=customer_id, return_url=serializer.validated_data.get("return_url"), ) except StripeNotConfiguredError as exc: return Response( {"detail": str(exc)}, status=status.HTTP_503_SERVICE_UNAVAILABLE, ) except stripe.StripeError as exc: logger.exception("Stripe Billing Portal Session creation failed") return Response( {"detail": str(getattr(exc, "user_message", None) or exc)}, status=status.HTTP_502_BAD_GATEWAY, ) return Response( {"portal_url": session.url}, status=status.HTTP_201_CREATED, ) class InvoiceListView(APIView): def get(self, request): invoices = Invoice.objects.filter(user=request.user) return Response(InvoiceSerializer(invoices, many=True).data) class PaymentListView(APIView): def get(self, request): payments = Payment.objects.filter(user=request.user) return Response(PaymentSerializer(payments, many=True).data) class PlanListView(APIView): """Public-facing plan catalog (only `is_public` rows by default).""" permission_classes = (permissions.AllowAny,) authentication_classes = () def get(self, request): seed_subscription_plans(update_existing=False) include_all = ( request.user and request.user.is_authenticated and request.user.is_staff and request.query_params.get("all") == "1" ) qs = SubscriptionPlan.objects.all() if not include_all: qs = qs.filter(is_public=True) return Response(SubscriptionPlanSerializer(qs, many=True).data) class SubscriptionMeView(APIView): """Current user's plan, checkout need, and usage snapshot (#16/#17/#36).""" def get(self, request): seed_subscription_plans(update_existing=False) try: sub = request.user.subscription except UserSubscription.DoesNotExist: sub = None usage = get_usage_snapshot(request.user) payload = { "plan": plan_to_dict(sub.plan) if sub and sub.plan_id else None, "status": sub.status if sub else UserSubscription.Status.NONE, "source": sub.source if sub else UserSubscription.Source.NONE, "needs_checkout": needs_checkout(request.user), "stripe_subscription_id": ( sub.stripe_subscription_id if sub else "" ), "usage": usage.to_dict(), } return Response(payload) class StripeWebhookView(APIView): """Verify Stripe signatures and upsert local invoice/payment rows.""" permission_classes = (permissions.AllowAny,) authentication_classes = () def post(self, request): payload = request.body sig_header = request.META.get("HTTP_STRIPE_SIGNATURE", "") webhook_secret = settings.STRIPE_WEBHOOK_SECRET if not webhook_secret: logger.error("STRIPE_WEBHOOK_SECRET is not configured") return Response( {"detail": "Webhook secret not configured"}, status=status.HTTP_503_SERVICE_UNAVAILABLE, ) try: event = stripe.Webhook.construct_event( payload=payload, sig_header=sig_header, secret=webhook_secret, ) except ValueError: return Response( {"detail": "Invalid payload"}, status=status.HTTP_400_BAD_REQUEST, ) except stripe.SignatureVerificationError: return Response( {"detail": "Invalid signature"}, status=status.HTTP_400_BAD_REQUEST, ) if hasattr(event, "to_dict"): event = event.to_dict() try: dispatch_stripe_event(event) except Exception: logger.exception("Error handling Stripe event %s", event.get("id")) return Response( {"detail": "Webhook handler error"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) return Response({"received": True}, status=status.HTTP_200_OK)