import logging import stripe from rest_framework.request import Request from rest_framework.response import Response from rest_framework.mixins import ListModelMixin from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import IsAuthenticated from rest_framework import status from django.views.decorators.csrf import csrf_exempt from django.db.models import QuerySet from django.conf import settings from django.utils.decorators import method_decorator from django.views import View from django.http import ( HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, ) from labs.api.bases.billing import BillingBaseEndpoint from labs.billing.providers.stripe import ( StripeProvider, stripe_webhook_payment_intent_succeeded, ) from labs.billing.user import get_balance, get_user_billing_profile from labs.billing.payment import ( calculate_price_when_buy_credits, create_credit_order, checkout_order, ) from labs.billing.exceptions import ( InvalidAmountError, StripeAttachPaymentMethodError, CreateOrderError, CheckoutOrderError, ) from labs.billing.consts import ( BILLING_ALLOWED_COUNTRIES, BUY_CREDITS_MIN_QUANTITY, BUY_CREDITS_MAX_QUANTITY, BUY_CREDITS_DEFAULT_QUANTITY, STRIPE_MAX_PAYMENT_METHODS, STRIPE_ALLOWED_PAYMENT_METHOD_TYPES, ) from labs.api.serializers.billing import ( TransactionListSerializer, PaymentCreditsSerializer, UserProfileUpdateBillingDetailsSerializer, StripePaymentMethodSerializer, CheckoutOrderSerializer, ) from labs.models import CreditTransaction, BillingCreditOrder logger = logging.getLogger(__name__) class BillingConfigEndpoint(BillingBaseEndpoint): permission_classes = (IsAuthenticated,) def get(self, request: Request, *args, **kwargs) -> Response: return Response( { "allowed_countries": BILLING_ALLOWED_COUNTRIES, "buy_credits": { "min_quantity": BUY_CREDITS_MIN_QUANTITY, "max_quantity": BUY_CREDITS_MAX_QUANTITY, "default_quantity": BUY_CREDITS_DEFAULT_QUANTITY, }, "stripe": { "public_key": settings.STRIPE_PUBLIC_KEY, "allowed_payment_method_types": STRIPE_ALLOWED_PAYMENT_METHOD_TYPES, "max_payment_methods": STRIPE_MAX_PAYMENT_METHODS, }, } ) class BalanceEndpoint(BillingBaseEndpoint): permission_classes = (IsAuthenticated,) def get(self, request: Request) -> Response: return Response(get_balance(request.user)) class TransactionPagination(PageNumberPagination): page_size = 20 page_size_query_param = "page_size" max_page_size = 20 class TransactionListEndpoint(ListModelMixin, BillingBaseEndpoint): serializer_class = TransactionListSerializer pagination_class = TransactionPagination permission_classes = (IsAuthenticated,) def get_queryset(self) -> QuerySet[CreditTransaction]: return ( CreditTransaction.objects.select_related("redemption") .filter(user=self.request.user) .order_by("-created_at") ) def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) class CalculatePriceEndpoint(BillingBaseEndpoint): """ Pre-calculate the price and discount when buying credits, this endpoint will not include tax in the calculation. """ def post(self, request: Request, *args, **kwargs) -> Response: serializer = PaymentCreditsSerializer(data=request.data) serializer.is_valid(raise_exception=True) quantity = serializer.validated_data["quantity"] try: result = calculate_price_when_buy_credits(quantity, include_tax=False) except InvalidAmountError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response(result.to_frontend()) class UserProfileEndpoint(BillingBaseEndpoint): def get(self, request: Request, *args, **kwargs) -> Response: billing_details, stripe_payment_methods = get_user_billing_profile(request.user) return Response( { "billing_details": billing_details, "stripe_payment_methods": stripe_payment_methods, } ) class UserProfileValidateBillingDetailsEndpoint(BillingBaseEndpoint): """ Only validate the billing details, do not save them. Frontend will call this endpoint to check if the billing details are valid when add new payment method. """ def post(self, request: Request, *args, **kwargs) -> Response: serializer = UserProfileUpdateBillingDetailsSerializer( data=request.data, context={"request": request}, ) serializer.is_valid(raise_exception=True) return Response() class UserProfileUpdateBillingDetailsEndpoint(BillingBaseEndpoint): def put(self, request: Request, *args, **kwargs) -> Response: serializer = UserProfileUpdateBillingDetailsSerializer( data=request.data, context={"request": request}, ) serializer.is_valid(raise_exception=True) serializer.save() return Response() class UserStripeCreateSetupIntentEndpoint(BillingBaseEndpoint): def post(self, request: Request, *args, **kwargs) -> Response: try: client_secret = StripeProvider(request.user, request).create_setup_intent() except StripeAttachPaymentMethodError as e: return Response( {"non_field_errors": [str(e)]}, status=status.HTTP_400_BAD_REQUEST, ) return Response({"client_secret": client_secret}) class UserStripeAttachPaymentMethodEndpoint(BillingBaseEndpoint): def post(self, request: Request, *args, **kwargs) -> Response: serializer = StripePaymentMethodSerializer(data=request.data) serializer.is_valid(raise_exception=True) payment_method_id = serializer.validated_data["payment_method_id"] try: StripeProvider(request.user, request).attach_payment_method_to_customer( payment_method_id ) except StripeAttachPaymentMethodError as e: return Response( {"payment_method_id": [str(e)]}, status=status.HTTP_400_BAD_REQUEST, ) return Response() class UserStripeDetachPaymentMethodEndpoint(BillingBaseEndpoint): def post(self, request: Request, *args, **kwargs) -> Response: serializer = StripePaymentMethodSerializer(data=request.data) serializer.is_valid(raise_exception=True) payment_method_id = serializer.validated_data["payment_method_id"] StripeProvider(request.user, request).detach_payment_method_from_customer( payment_method_id ) return Response() class BillingCreditOrderEndpoint(BillingBaseEndpoint): def post(self, request: Request, *args, **kwargs) -> Response: serializer = PaymentCreditsSerializer(data=request.data) serializer.is_valid(raise_exception=True) quantity = serializer.validated_data["quantity"] try: order = create_credit_order(request.user, quantity) except CreateOrderError as e: return Response( {"non_field_errors": [str(e)]}, status=status.HTTP_400_BAD_REQUEST ) return Response(order.to_frontend()) class BillingCreditOrderCheckoutEndpoint(BillingBaseEndpoint): def post(self, request: Request, order_id: int, *args, **kwargs) -> Response: try: order = BillingCreditOrder.objects.get( id=order_id, user=request.user, ) except BillingCreditOrder.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) serializer = CheckoutOrderSerializer( data=request.data, context={"request": request} ) serializer.is_valid(raise_exception=True) payment_method_id = serializer.validated_data["payment_method_id"] try: payment_status, payment_status_details = checkout_order( request, order, payment_method_id ) except CheckoutOrderError as e: return Response( {"non_field_errors": [str(e)]}, status=status.HTTP_400_BAD_REQUEST ) return Response({"status": payment_status, "data": payment_status_details}) class BillingGetOrderDetailsEndpoint(BillingBaseEndpoint): def get(self, request: Request, order_id: int, *args, **kwargs) -> Response: order = BillingCreditOrder.objects.get( id=order_id, user=request.user, ) return Response(order.to_frontend()) @method_decorator(csrf_exempt, name="dispatch") class StripeWebhookEndpoint(View): def post(self, request: Request, *args, **kwargs) -> Response: payload = request.body sig_header = request.META.get("HTTP_STRIPE_SIGNATURE", "") webhook_secret = settings.STRIPE_WEBHOOK_SECRET try: event = stripe.Webhook.construct_event( payload=payload, sig_header=sig_header, secret=webhook_secret, ) except ValueError: logger.error(f"[Stripe webhook] Invalid payload: {payload}") return HttpResponseBadRequest("Invalid payload") except stripe.error.SignatureVerificationError: logger.error(f"[Stripe webhook] Invalid signature: {sig_header}") return HttpResponseForbidden("Invalid signature") except Exception as e: logger.error(f"[Stripe webhook] Error: {e}") return HttpResponseBadRequest(str(e)) if event.type == "payment_intent.succeeded": payment_intent = event.data.object if payment_intent.metadata.get("platform") == "lab": stripe_webhook_payment_intent_succeeded(payment_intent.id) else: logger.info( f"[Stripe webhook] Payment intent {payment_intent.id} not from Lab" ) return HttpResponse()