From 1a2013e10b1e3b04e2bd63f74195c7300adb37bc Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Mon, 12 Aug 2024 12:54:29 +0530 Subject: [PATCH] my subscriptions page --- goodtimes/webhook/subscription_service.py | 1 + manage_coupons/forms.py | 28 +-- manage_coupons/models.py | 41 +++++ manage_subscriptions/forms.py | 2 - manage_subscriptions/models.py | 16 ++ manage_subscriptions/urls.py | 11 +- manage_subscriptions/views.py | 161 +++++++++++++----- .../manage_subscriptions/product_list.html | 25 +-- .../subscription_details.html | 77 +++++++++ .../subscription_list.html | 8 +- .../stripe_html/active_subscription.html | 70 ++++++++ 11 files changed, 337 insertions(+), 103 deletions(-) create mode 100644 templates/manage_subscriptions/subscription_details.html create mode 100644 templates/stripe_html/active_subscription.html diff --git a/goodtimes/webhook/subscription_service.py b/goodtimes/webhook/subscription_service.py index 0940563..68770f1 100644 --- a/goodtimes/webhook/subscription_service.py +++ b/goodtimes/webhook/subscription_service.py @@ -38,6 +38,7 @@ class SubscriptionService: subscription=subscription, stripe_subscription_id=stripe_subscription or "Non Recurring", is_paid=True, + auto_renew=bool(stripe_subscription), is_stripe_subscription=bool(stripe_subscription), order_id=order_id, start_date=start_date, diff --git a/manage_coupons/forms.py b/manage_coupons/forms.py index e40b3eb..22874e5 100644 --- a/manage_coupons/forms.py +++ b/manage_coupons/forms.py @@ -9,7 +9,7 @@ class CouponForm(forms.ModelForm): fields = [ "title", "description", - "image", + # "image", "discount_amount", "discount_percentage", "valid_from", @@ -19,28 +19,6 @@ class CouponForm(forms.ModelForm): widgets = { "valid_from": forms.DateTimeInput(attrs={"type": "datetime-local"}), "valid_to": forms.DateTimeInput(attrs={"type": "datetime-local"}), + # "discount_amount": forms.NumberInput(attrs={'step': '0.01'}), + # "discount_percentage": forms.NumberInput(attrs={'step': '0.01'}), } - - def clean(self): - cleaned_data = super().clean() - discount_amount = cleaned_data.get("discount_amount") - discount_percentage = cleaned_data.get("discount_percentage") - valid_from = cleaned_data.get("valid_from") - valid_to = cleaned_data.get("valid_to") - - if discount_amount and discount_percentage: - raise ValidationError( - "You can only set either a discount amount or a discount percentage, not both." - ) - - if not discount_amount and not discount_percentage: - raise ValidationError( - "You must set either a discount amount or a discount percentage." - ) - - if valid_from and valid_to and valid_from >= valid_to: - raise ValidationError( - "The valid_from date must be earlier than the valid_to date." - ) - - return cleaned_data diff --git a/manage_coupons/models.py b/manage_coupons/models.py index 25ce934..743ce43 100644 --- a/manage_coupons/models.py +++ b/manage_coupons/models.py @@ -1,6 +1,7 @@ from django.db import models from django.utils import timezone from accounts.models import BaseModel, IAmPrincipalType +from django.core.exceptions import ValidationError class Coupon(BaseModel): @@ -23,6 +24,46 @@ class Coupon(BaseModel): class Meta: db_table = "coupon" + def clean(self): + """ + Validate the Coupon instance. Ensure that the `max_redeems` is greater than 0, + that either `discount_amount` or `discount_percentage` is set, and that + `valid_from` is earlier than `valid_to`. + """ + if self.max_redeems < 1: + raise ValidationError({"max_redeems": "Redeems must be more than 1."}) + + # Ensure discount_amount is non-negative + if self.discount_amount is not None and self.discount_amount < 1: + raise ValidationError( + {"discount_amount": "Discount amount must be more than 1."} + ) + + # Ensure discount_percentage is non-negative + if self.discount_percentage is not None and self.discount_percentage < 1: + raise ValidationError( + {"discount_percentage": "Discount percentage must be more than 1."} + ) + + if self.discount_amount and self.discount_percentage: + raise ValidationError( + "You can only set either a discount amount or a discount percentage, not both." + ) + + if not self.discount_amount and not self.discount_percentage: + raise ValidationError( + "You must set either a discount amount or a discount percentage." + ) + + if self.valid_from and self.valid_to and self.valid_from >= self.valid_to: + raise ValidationError( + "The valid_from date must be earlier than the valid_to date." + ) + + def save(self, *args, **kwargs): + self.clean() # Call clean before saving to ensure validation + super().save(*args, **kwargs) + def __str__(self): return self.coupon_code diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index ffbe30e..12294f5 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -31,8 +31,6 @@ class SubscriptionForm(forms.ModelForm): "high_amount", "amount", "short_description", - # "long_description", - # "image", "principal_types", "referral_percentage", "active", diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index 0aea65d..0c3ec19 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -1,5 +1,6 @@ from datetime import timedelta, timezone from django.db import models +from django.core.exceptions import ValidationError from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType from django.utils.translation import gettext_lazy as _ @@ -65,6 +66,21 @@ class Subscription(BaseModel): def __str__(self): return self.title + def clean(self): + # Ensure amount is greater than 1 + if self.amount <= 1: + raise ValidationError({"amount": "Amount must be greater than 1."}) + + # Ensure high_amount is greater than amount + if self.high_amount <= self.amount: + raise ValidationError( + {"high_amount": "High amount must be greater than amount."} + ) + + def save(self, *args, **kwargs): + self.clean() # Call clean before saving to ensure validation + super().save(*args, **kwargs) + class SubscriptionStatus(models.TextChoices): ACTIVE = "active", _("Active") diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index fee84c5..840de54 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ views.SubscriptionCreateOrUpdateView.as_view(), name="subscription_add", ), + path("subscription//", views.SubscriptionDetailView.as_view(), name="subscription_detail"), # path( # "subscription/edit//", # views.SubscriptionCreateOrUpdateView.as_view(), @@ -31,11 +32,11 @@ urlpatterns = [ views.StripeProductCreateOrUpdateView.as_view(), name="stripe_product_add", ), - path( - "product/delete/", - views.StripeProductDeleteView.as_view(), - name="stripe_product_delete", - ), + # path( + # "product/delete/", + # views.StripeProductDeleteView.as_view(), + # name="stripe_product_delete", + # ), # PLANS path("plan/list/", views.PlanView.as_view(), name="plan_list"), # path( diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 23cb784..cfb8348 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -21,7 +21,13 @@ from manage_wallets.models import ( TransactionStatus, TransactionType, ) -from .models import Plan, StripeProduct, Subscription, PrincipalSubscription +from .models import ( + Plan, + StripeProduct, + Subscription, + PrincipalSubscription, + SubscriptionStatus, +) from django.views import generic from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy @@ -179,6 +185,20 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView): return context +class SubscriptionDetailView(generic.DetailView): + 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_details.html" + context_object_name = "subscription" + + 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 @@ -325,44 +345,61 @@ class StripeProductView(LoginRequiredMixin, generic.ListView): 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 +""" we are not using product delete functionality because there may be multiple stripe's prices + attached to one product and in case of any error it will mismatch the stripe's price with + our database Subscription objects""" +# 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) +# 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 +# # Fetching the related subscriptions (prices) +# related_subscriptions = Subscription.objects.filter(stripe_product=product) - 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) +# # 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 - # Updating the subscription model record - product.deleted = True - product.active = False - product.save() +# # Deactivating related prices on Stripe first +# for subscription in related_subscriptions: +# price_id = subscription.price_id +# if price_id: +# try: +# stripe.Price.modify(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) - messages.success(request, self.success_message) +# 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) - except self.model.DoesNotExist: - messages.error(request, self.error_message) +# # Updating the subscription model record +# product.deleted = True +# product.active = False +# product.save() - return redirect(self.success_url) +# 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): @@ -572,6 +609,32 @@ class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View): class SubscriptionPageView(TemplateView): template_name = "stripe_html/index.html" + 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 + + +class ActiveSubscriptionView(TemplateView): + template_name = "stripe_html/active_subscription.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") @@ -608,22 +671,25 @@ class SubscriptionPageView(TemplateView): 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, + active_subscription = ( + PrincipalSubscription.objects.filter( + principal=request.user, + is_paid=True, + cancelled=False, + deleted=False, + active=True, + status=SubscriptionStatus.ACTIVE, + ) + .order_by("-grace_period_end_date") + .first() ) - 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 + if active_subscription: + context["active_subscription"] = active_subscription else: - # Handling the case where no subscriptions are found for the principal type. - context["error"] = "No subscriptions found for your user type." + # If no active subscription is found, redirect to the SubscriptionPageView + return redirect("manage_subscriptions:stripe") + return context @@ -691,7 +757,10 @@ def validate_coupon(request): return JsonResponse( {"error": "Coupon max redeems reached."}, status=400 ) - return JsonResponse({"data": {"coupon": stripe_coupon, "finalAmount": final_amount}}, status=200) + return JsonResponse( + {"data": {"coupon": stripe_coupon, "finalAmount": final_amount}}, + status=200, + ) except stripe.error.InvalidRequestError: return JsonResponse( {"error": f"Invalid coupon code: {coupon_code}"}, status=400 diff --git a/templates/manage_subscriptions/product_list.html b/templates/manage_subscriptions/product_list.html index 6cc9c14..3551f4b 100644 --- a/templates/manage_subscriptions/product_list.html +++ b/templates/manage_subscriptions/product_list.html @@ -49,9 +49,6 @@ style="width: 69.2656px;"> Stripe Product ID Active - Action @@ -63,29 +60,9 @@ {{data_obj.active}} - - - + {% endfor %} - diff --git a/templates/manage_subscriptions/subscription_details.html b/templates/manage_subscriptions/subscription_details.html new file mode 100644 index 0000000..44833a7 --- /dev/null +++ b/templates/manage_subscriptions/subscription_details.html @@ -0,0 +1,77 @@ +{% extends 'layout/base_template.html' %} +{% load static %} + +{% block content %} +
+
+ +
+
+
+ {% if subscription.image %} + {{ subscription.title }} + {% endif %} +

{{ subscription.title }}

+
+
+
+ + +
+
+
+
Plan & Pricing
+
+
+

Plan: {{ subscription.plan.title }}

+

Price ID: {{ subscription.price_id|default:"Not a Stripe Subscription" }}

+

Stripe Product: {{ subscription.stripe_product|default:"None" }}

+

Amount: ${{ subscription.amount }}

+

High Amount: ${{ subscription.high_amount }}

+

Referral Percentage: {{ subscription.referral_percentage }}%

+

Is Free: + {% if subscription.is_free %} + Yes + {% else %} + No + {% endif %} +

+
+
+
+ + +
+
+
+
Descriptions
+
+
+

Short Description: {{ subscription.short_description|default:"Not Provided" }}

+

Long Description:

+

{{ subscription.long_description|default:"Not Provided" }}

+
+
+
+ + +
+
+
+
Principal Types
+
+
+
    + {% for principal_type in subscription.principal_types.all %} +
  • {{ principal_type.name }}
  • + {% endfor %} +
+
+
+
+ +
+
+ +{% endblock content %} + \ No newline at end of file diff --git a/templates/manage_subscriptions/subscription_list.html b/templates/manage_subscriptions/subscription_list.html index c52486d..41afe54 100644 --- a/templates/manage_subscriptions/subscription_list.html +++ b/templates/manage_subscriptions/subscription_list.html @@ -115,7 +115,13 @@ - +
  • + + + visibility + + +
  • diff --git a/templates/stripe_html/active_subscription.html b/templates/stripe_html/active_subscription.html new file mode 100644 index 0000000..06f440a --- /dev/null +++ b/templates/stripe_html/active_subscription.html @@ -0,0 +1,70 @@ + + + + + + Active Subscription + + + + +
    +
    +
    +

    Your Active Subscription

    +
    +
    + +
    +
    +
    Subscription:
    +

    {{ active_subscription.subscription.name }}

    +
    +
    +
    Principal:
    +

    {{ active_subscription.principal.first_name }} {{ active_subscription.principal.last_name }}

    +
    +
    + +
    +
    +
    Status:
    +

    {{ active_subscription.get_status_display }}

    +
    +
    +
    Start Date:
    +

    {{ active_subscription.start_date }}

    +
    +
    + +
    +
    +
    End Date:
    +

    {{ active_subscription.end_date }}

    +
    +
    +
    Auto Renew:
    +

    {{ active_subscription.auto_renew|yesno:"Yes,No" }}

    +
    +
    + + +
    +
    +
    Coupon Code:
    +

    {{ active_subscription.coupon_code }}

    +
    +
    + + + +
    +
    +
    + + + + +