from decimal import Decimal import json from django.http import HttpResponseBadRequest, JsonResponse from django.shortcuts import get_object_or_404, redirect, render import stripe from accounts import resource_action from accounts.models import IAmPrincipal from django.contrib.auth import login import jwt from django.utils import timezone from django.contrib.auth import get_user_model from manage_coupons.models import Coupon from manage_subscriptions.forms import ( StripeProductForm, SubscriptionForm, PrincipalSubscriptionForm, ) from manage_wallets.models import ( PaymentMethod, Transaction, TransactionStatus, TransactionType, ) from .models import Plan, StripeProduct, Subscription, PrincipalSubscription from django.views import generic from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy from django.contrib import messages from goodtimes import constants from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.conf import settings from django.views.generic.base import TemplateView from django.db.models import Q # Create your views here. class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS # Initialize the action as ACTION_CREATE (can change based on logic) action = resource_action.ACTION_CREATE # Default action template_name = "manage_subscriptions/subscription_add.html" model = Subscription form_class = SubscriptionForm success_url = reverse_lazy("manage_subscriptions:subscription_list") error_message = "An error occurred while saving the data." # Determine the success message dynamically based on whether it's an update or create def get_success_message(self): self.success_message = ( constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED ) return self.success_message # Get the object (if exists) based on URL parameter 'pk' def get_object(self): pk = self.kwargs.get("pk") return get_object_or_404(self.model, pk=pk) if pk else None # Add page_name and operation to the context def get_context_data(self, **kwargs): context = { "page_name": self.page_name, "operation": "Add" if not self.object else "Edit", } context.update(kwargs) # Include any additional context data passed to the view return context def get(self, request, *args, **kwargs): self.object = self.get_object() # If an object is found, change action to ACTION_UPDATE if self.object is not None: self.action = resource_action.ACTION_UPDATE form = self.form_class(instance=self.object) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) def post(self, request, *args, **kwargs): self.object = self.get_object() # If an object is found, change action to ACTION_UPDATE if self.object is not None: self.action = resource_action.ACTION_UPDATE form = self.form_class(request.POST, instance=self.object) if not form.is_valid(): print(form.errors) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) # This code ensures that only one free plan can be created by checking for existing free plans before saving a new one. if form.cleaned_data.get("is_free"): if self.model.objects.filter(Q(is_free=True) & Q(active=True)).exists(): messages.error( self.request, "A free plan is already available. Please deactivate the existing one before creating a new one.", ) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) # Processing Stripe price creation and handling free subscription success, message = self.handle_stripe_price(form) if not success: messages.error(self.request, message) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) form.save() messages.success(self.request, self.get_success_message()) return redirect(self.success_url) def handle_stripe_price(self, form): try: stripe.api_key = settings.STRIPE_SECRET_KEY # creating Stripe price only if the subscription is not free if not form.cleaned_data.get("is_free"): # Getting Stripe Product ID stripe_product = form.instance.stripe_product plan = form.instance.plan # Map the Plan interval to Stripe's recurring interval # It will only work if the plan title is 'month', 'year' 'week' or 'day' stripe_interval = plan.title # Create the Stripe price stripe_price = stripe.Price.create( unit_amount=int( form.cleaned_data["amount"] * 100 ), # Amount in cents currency="gbp", # Adjust the currency as needed recurring={ "interval": stripe_interval }, # Use the interval from Plan product=stripe_product.product_id, ) # Assign the Stripe price ID to the subscription form.instance.price_id = stripe_price.id else: form.instance.price_id = None # No price ID for free subscriptions return True, "" # Success except stripe.error.StripeError as e: return False, f"Stripe error: {str(e)}" except Exception as e: return False, f"An error occurred: {str(e)}" class SubscriptionView(LoginRequiredMixin, generic.ListView): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS action = resource_action.ACTION_READ model = Subscription template_name = "manage_subscriptions/subscription_list.html" context_object_name = "subscription_obj" def get_queryset(self): queryset = ( super() .get_queryset() .filter(deleted=False, active=True) .prefetch_related("principal_types") ) return queryset.order_by("-created_on") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_name"] = self.page_name return context class SubscriptionDeleteView(LoginRequiredMixin, generic.View): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS action = resource_action.ACTION_DELETE model = Subscription success_url = reverse_lazy("manage_subscriptions:subscription_list") success_message = constants.RECORD_DELETED error_message = constants.RECORD_NOT_FOUND def get(self, request, pk): try: # Retrieve the subscription object subscription = self.model.objects.get(id=pk) # Checking if there is a Stripe Price ID associated with the subscription stripe_price_id = subscription.price_id if stripe_price_id: stripe.api_key = settings.STRIPE_SECRET_KEY try: # Updating the Stripe price to mark it as inactive stripe.Price.modify(stripe_price_id, active=False) except stripe.error.StripeError as e: # Handle Stripe errors messages.error(request, f"Stripe error: {str(e)}") return redirect(self.success_url) # Updating the subscription model record subscription.deleted = True subscription.active = False subscription.save() messages.success(request, self.success_message) except self.model.DoesNotExist: messages.error(request, self.error_message) return redirect(self.success_url) class StripeProductCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS # Initialize the action as ACTION_CREATE (can change based on logic) action = resource_action.ACTION_CREATE # Default action template_name = "manage_subscriptions/product_add.html" model = StripeProduct form_class = StripeProductForm success_url = reverse_lazy("manage_subscriptions:stripe_product_list") error_message = "An error occurred while saving the data." # Determine the success message dynamically based on whether it's an update or create def get_success_message(self): self.success_message = ( constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED ) return self.success_message # Get the object (if exists) based on URL parameter 'pk' def get_object(self): pk = self.kwargs.get("pk") return get_object_or_404(self.model, pk=pk) if pk else None # Add page_name and operation to the context def get_context_data(self, **kwargs): context = { "page_name": self.page_name, "operation": "Add" if not self.object else "Edit", } context.update(kwargs) # Include any additional context data passed to the view return context def get(self, request, *args, **kwargs): self.object = self.get_object() # If an object is found, change action to ACTION_UPDATE if self.object is not None: self.action = resource_action.ACTION_UPDATE form = self.form_class(instance=self.object) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) def post(self, request, *args, **kwargs): self.object = self.get_object() # If an object is found, change action to ACTION_UPDATE if self.object is not None: self.action = resource_action.ACTION_UPDATE form = self.form_class(request.POST, instance=self.object) if not form.is_valid(): print(form.errors) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) success, message = self.handle_stripe_product(form) if not success: messages.error(self.request, message) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) form.save() messages.success(self.request, self.get_success_message()) return redirect(self.success_url) def handle_stripe_product(self, form): try: stripe.api_key = settings.STRIPE_SECRET_KEY stripe_product = stripe.Product.create( name=form.cleaned_data.get("title"), description=form.cleaned_data.get("description"), ) # Save Stripe Product ID to the form instance form.instance.product_id = stripe_product.id return True, "" # Success except stripe.error.StripeError as e: return False, f"Stripe error: {str(e)}" except Exception as e: return False, f"An error occurred: {str(e)}" class StripeProductView(LoginRequiredMixin, generic.ListView): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS action = resource_action.ACTION_READ model = StripeProduct template_name = "manage_subscriptions/product_list.html" context_object_name = "product_obj" def get_queryset(self): queryset = super().get_queryset().filter(deleted=False, active=True) return queryset.order_by("-created_on") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_name"] = self.page_name return context class StripeProductDeleteView(LoginRequiredMixin, generic.View): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS action = resource_action.ACTION_DELETE model = StripeProduct success_url = reverse_lazy("manage_subscriptions:stripe_product_list") success_message = constants.RECORD_DELETED error_message = constants.RECORD_NOT_FOUND def get(self, request, pk): try: # Retrieve the subscription object product = self.model.objects.get(id=pk) # Checking if there is a Stripe Product ID associated with the subscription stripe_product_id = product.product_id if stripe_product_id: stripe.api_key = settings.STRIPE_SECRET_KEY try: # Updating the Stripe price to mark it as inactive stripe.Product.modify(stripe_product_id, active=False) except stripe.error.StripeError as e: # Handle Stripe errors messages.error(request, f"Stripe error: {str(e)}") return redirect(self.success_url) # Updating the subscription model record product.deleted = True product.active = False product.save() messages.success(request, self.success_message) except self.model.DoesNotExist: messages.error(request, self.error_message) return redirect(self.success_url) # class PlanCreateOrUpdateView(LoginRequiredMixin, generic.View): # # Set the page_name and resource # page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS # resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS # # Initialize the action as ACTION_CREATE (can change based on logic) # action = resource_action.ACTION_CREATE # Default action # template_name = "manage_subscriptions/plan_add.html" # model = Plan # form_class = PlanForm # success_url = reverse_lazy("manage_subscriptions:plan_list") # error_message = "An error occurred while saving the data." # # Determine the success message dynamically based on whether it's an update or create # def get_success_message(self): # self.success_message = ( # constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED # ) # return self.success_message # # Get the object (if exists) based on URL parameter 'pk' # def get_object(self): # pk = self.kwargs.get("pk") # return get_object_or_404(self.model, pk=pk) if pk else None # # Add page_name and operation to the context # def get_context_data(self, **kwargs): # context = { # "page_name": self.page_name, # "operation": "Add" if not self.object else "Edit", # } # context.update(kwargs) # Include any additional context data passed to the view # return context # def get(self, request, *args, **kwargs): # self.object = self.get_object() # # If an object is found, change action to ACTION_UPDATE # if self.object is not None: # self.action = resource_action.ACTION_UPDATE # form = self.form_class(instance=self.object) # context = self.get_context_data(form=form) # return render(request, self.template_name, context=context) # def post(self, request, *args, **kwargs): # self.object = self.get_object() # # If an object is found, change action to ACTION_UPDATE # if self.object is not None: # self.action = resource_action.ACTION_UPDATE # form = self.form_class(request.POST, instance=self.object) # if not form.is_valid(): # print(form.errors) # context = self.get_context_data(form=form) # return render(request, self.template_name, context=context) # form.save() # messages.success(self.request, self.get_success_message()) # return redirect(self.success_url) class PlanView(LoginRequiredMixin, generic.ListView): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS action = resource_action.ACTION_READ model = Plan template_name = "manage_subscriptions/plan_list.html" context_object_name = "plan_obj" def get_queryset(self): return super().get_queryset().filter(deleted=False) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_name"] = self.page_name return context # class PlanDeleteView(LoginRequiredMixin, generic.View): # page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS # resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS # action = resource_action.ACTION_DELETE # model = Plan # success_url = reverse_lazy("manage_subscriptions:plan_list") # success_message = constants.RECORD_DELETED # error_message = constants.RECORD_NOT_FOUND # def get(self, request, pk): # try: # type_obj = self.model.objects.get(id=pk) # type_obj.deleted = True # type_obj.active = False # type_obj.save() # messages.success(request, self.success_message) # except self.model.DoesNotExist: # messages.success(request, self.error_message) # return redirect(self.success_url) class PrincipalSubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS # Initialize the action as ACTION_CREATE (can change based on logic) action = resource_action.ACTION_CREATE # Default action template_name = "manage_subscriptions/principal_subscription_add.html" model = PrincipalSubscription form_class = PrincipalSubscriptionForm success_url = reverse_lazy("manage_subscriptions:principal_subscriptions_list") error_message = "An error occurred while saving the data." # Determine the success message dynamically based on whether it's an update or create def get_success_message(self): self.success_message = ( constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED ) return self.success_message # Get the object (if exists) based on URL parameter 'pk' def get_object(self): pk = self.kwargs.get("pk") return get_object_or_404(self.model, pk=pk) if pk else None # Add page_name and operation to the context def get_context_data(self, **kwargs): context = { "page_name": self.page_name, "operation": "Add" if not self.object else "Edit", } context.update(kwargs) # Include any additional context data passed to the view return context def get(self, request, *args, **kwargs): self.object = self.get_object() # If an object is found, change action to ACTION_UPDATE if self.object is not None: self.action = resource_action.ACTION_UPDATE form = self.form_class(instance=self.object) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) def post(self, request, *args, **kwargs): self.object = self.get_object() # If an object is found, change action to ACTION_UPDATE if self.object is not None: self.action = resource_action.ACTION_UPDATE form = self.form_class(request.POST, instance=self.object) if not form.is_valid(): print(form.errors) context = self.get_context_data(form=form) return render(request, self.template_name, context=context) form.save() messages.success(self.request, self.get_success_message()) return redirect(self.success_url) class PrincipalSubscriptionView(LoginRequiredMixin, generic.ListView): page_name = resource_action.RESOURCE_PRINCIPAL_SUBSCRIPTIONS resource = resource_action.RESOURCE_PRINCIPAL_SUBSCRIPTIONS action = resource_action.ACTION_READ model = PrincipalSubscription template_name = "manage_subscriptions/principal_subscriptions_list.html" context_object_name = "principal_subscription_obj" def get_queryset(self): return super().get_queryset().all() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_name"] = self.page_name return context class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS action = resource_action.ACTION_DELETE model = PrincipalSubscription success_url = reverse_lazy("manage_subscriptions:principal_subscriptions_list") success_message = constants.RECORD_DELETED error_message = constants.RECORD_NOT_FOUND def get(self, request, pk): try: type_obj = self.model.objects.get(id=pk) type_obj.deleted = True type_obj.active = False type_obj.save() messages.success(request, self.success_message) except self.model.DoesNotExist: messages.success(request, self.error_message) return redirect(self.success_url) class SubscriptionPageView(TemplateView): template_name = "stripe_html/index.html" def get(self, request, *args, **kwargs): # Example of extracting the token from a query parameter or cookie token = request.GET.get("token") or request.session.get("jwt") print("token: ", token) if token: request.session["jwt"] = token print("request.session: ", request.session) try: # Decode and validate token payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) print("payload: ", payload) try: UserModel = get_user_model() user = UserModel.objects.get(id=payload["user_id"]) # Manually specify the authentication backend user.backend = "django.contrib.auth.backends.ModelBackend" # Log the user in login(request, user) print("Logged in user: ", user) except IAmPrincipal.DoesNotExist: # Handle expired token return HttpResponseBadRequest("No Principal Found") except jwt.ExpiredSignatureError: # Handle expired token return HttpResponseBadRequest("Expired Signature Error") except jwt.InvalidTokenError: return HttpResponseBadRequest("Invalid Token Error") return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) request = self.request if request.user.is_authenticated: print("request.user: ", request.user) subscriptions = Subscription.objects.filter( principal_types=request.user.principal_type, active=True, deleted=False, is_free=False, ) if subscriptions.exists(): context["subscriptions"] = subscriptions context["stripeCheckoutUrl"] = settings.STRIPE_CHECKOUT_URL context["stripeFinalUrl"] = settings.STRIPE_FINAL_URL context["couponValidityCheckUrl"] = settings.COUPON_VALIDITY_CHECK_URL else: # Handling the case where no subscriptions are found for the principal type. context["error"] = "No subscriptions found for your user type." return context @csrf_exempt def stripe_config(request): if request.method == "GET": stripe_config = {"publicKey": settings.STRIPE_PUBLISH_KEY} return JsonResponse(stripe_config, safe=False) @csrf_exempt @require_POST def validate_coupon(request): data = json.loads(request.body) coupon_code = data.get("couponCode", None) subscription_id = data.get("subscriptionId", None) try: subscription = Subscription.objects.get(id=subscription_id) except Subscription.DoesNotExist: return JsonResponse({"error": "Subscription not found."}, status=404) # If no coupon code is provided, assume no discount and proceed if not coupon_code: return JsonResponse({"message": "No coupon code provided."}, status=200) # Validating Coupon try: coupon = Coupon.objects.get(coupon_code=coupon_code) if not coupon.is_valid(): return JsonResponse({"error": "Coupon is not valid."}, status=400) # Check discount amount if coupon.discount_amount and coupon.discount_amount > subscription.amount: return JsonResponse( {"error": "Coupon discount amount exceeds subscription amount."}, status=400, ) # Check discount percentage if coupon.discount_percentage: discount_amount = ( coupon.discount_percentage / Decimal("100") ) * subscription.amount if discount_amount > subscription.amount: return JsonResponse( { "error": "Coupon discount percentage exceeds subscription amount." }, status=400, ) # Retrieving coupon from Stripe if applicable if coupon.coupon_id: try: stripe_coupon = stripe.Coupon.retrieve(coupon.coupon_id) print("stripe_coupon: ", stripe_coupon) return JsonResponse({"data": {"coupon": stripe_coupon}}, status=200) except stripe.error.InvalidRequestError: return JsonResponse( {"error": f"Invalid coupon code: {coupon_code}"}, status=400 ) except stripe.error.StripeError as e: return JsonResponse({"error": f"Stripe error: {str(e)}"}, status=400) else: return JsonResponse( { "error": "Coupon is either invalid, expired, or not associated with Stripe." }, status=400, ) except Coupon.DoesNotExist: return JsonResponse({"error": "Coupon not found."}, status=404) @csrf_exempt @require_POST def create_checkout_session(request): stripe.api_key = settings.STRIPE_SECRET_KEY data = json.loads(request.body) subscription_id = data.get("subscriptionId") coupon_code = data.get("couponCode") principal_id = request.user.id try: subscription = Subscription.objects.get(id=subscription_id) except Subscription.DoesNotExist: return JsonResponse({"error": "Subscription not found."}, status=404) order_id = f"order_{timezone.localtime().timestamp()}_{request.user.email}" # Default transaction amount based on subscription amount final_amount = subscription.amount session_data = { "payment_method_types": ["card"], "success_url": request.build_absolute_uri("/subscriptions/success/"), "cancel_url": request.build_absolute_uri("/subscriptions/cancel/"), "metadata": { "principal": str(request.user.id), "order_id": order_id, "subscription_id": str(subscription.id), "transaction_id": "", "couponCode": coupon_code if coupon_code else None, }, } # Coupon Handling if coupon_code: try: stripe_coupon = stripe.Coupon.retrieve(coupon.coupon_id) session_data["discounts"] = [{"coupon": stripe_coupon.id}] except stripe.error.InvalidRequestError: return JsonResponse( {"error": f"Invalid coupon code: {coupon_code}"}, status=400 ) except stripe.error.StripeError as e: return JsonResponse( {"error": f"Stripe error: {str(e)}"}, status=400 ) # Create a Transaction object with status INITIATE transaction = Transaction.objects.create( principal=request.user, principal_subscription=None, # Subscription not created yet transaction_type=TransactionType.PAYMENT, payment_method=PaymentMethod.CARD, transaction_status=TransactionStatus.INITIATE, amount=final_amount, order_id=order_id, comment="Principal Subscription Initiated", ) # Updating transaction_id in session_data metadata session_data["metadata"]["transaction_id"] = str(transaction.id) # Creating the Stripe Checkout Session try: if subscription.price_id: session_data["line_items"] = [ { "price": subscription.price_id, "quantity": 1, } ] session_data["mode"] = "subscription" else: session_data["line_items"] = [ { "price_data": { "currency": "usd", "product_data": { "name": subscription.title, "description": subscription.short_description, }, "unit_amount": int( subscription.amount * 100 ), }, "quantity": 1, } ] session_data["mode"] = "payment" checkout_session = stripe.checkout.Session.create(**session_data) return JsonResponse({"sessionId": checkout_session["id"]}) except Exception as e: return JsonResponse({"error": str(e)}, status=500) class SuccessView(TemplateView): template_name = "stripe_html/success.html" class CancelView(TemplateView): template_name = "stripe_html/cancel.html"