my subscriptions page

This commit is contained in:
rizwanisready
2024-08-12 12:54:29 +05:30
parent 4c2d693ebf
commit 1a2013e10b
11 changed files with 337 additions and 103 deletions

View File

@@ -38,6 +38,7 @@ class SubscriptionService:
subscription=subscription, subscription=subscription,
stripe_subscription_id=stripe_subscription or "Non Recurring", stripe_subscription_id=stripe_subscription or "Non Recurring",
is_paid=True, is_paid=True,
auto_renew=bool(stripe_subscription),
is_stripe_subscription=bool(stripe_subscription), is_stripe_subscription=bool(stripe_subscription),
order_id=order_id, order_id=order_id,
start_date=start_date, start_date=start_date,

View File

@@ -9,7 +9,7 @@ class CouponForm(forms.ModelForm):
fields = [ fields = [
"title", "title",
"description", "description",
"image", # "image",
"discount_amount", "discount_amount",
"discount_percentage", "discount_percentage",
"valid_from", "valid_from",
@@ -19,28 +19,6 @@ class CouponForm(forms.ModelForm):
widgets = { widgets = {
"valid_from": forms.DateTimeInput(attrs={"type": "datetime-local"}), "valid_from": forms.DateTimeInput(attrs={"type": "datetime-local"}),
"valid_to": 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

View File

@@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from accounts.models import BaseModel, IAmPrincipalType from accounts.models import BaseModel, IAmPrincipalType
from django.core.exceptions import ValidationError
class Coupon(BaseModel): class Coupon(BaseModel):
@@ -23,6 +24,46 @@ class Coupon(BaseModel):
class Meta: class Meta:
db_table = "coupon" 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): def __str__(self):
return self.coupon_code return self.coupon_code

View File

@@ -31,8 +31,6 @@ class SubscriptionForm(forms.ModelForm):
"high_amount", "high_amount",
"amount", "amount",
"short_description", "short_description",
# "long_description",
# "image",
"principal_types", "principal_types",
"referral_percentage", "referral_percentage",
"active", "active",

View File

@@ -1,5 +1,6 @@
from datetime import timedelta, timezone from datetime import timedelta, timezone
from django.db import models from django.db import models
from django.core.exceptions import ValidationError
from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -65,6 +66,21 @@ class Subscription(BaseModel):
def __str__(self): def __str__(self):
return self.title 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): class SubscriptionStatus(models.TextChoices):
ACTIVE = "active", _("Active") ACTIVE = "active", _("Active")

View File

@@ -12,6 +12,7 @@ urlpatterns = [
views.SubscriptionCreateOrUpdateView.as_view(), views.SubscriptionCreateOrUpdateView.as_view(),
name="subscription_add", name="subscription_add",
), ),
path("subscription/<int:pk>/", views.SubscriptionDetailView.as_view(), name="subscription_detail"),
# path( # path(
# "subscription/edit/<int:pk>/", # "subscription/edit/<int:pk>/",
# views.SubscriptionCreateOrUpdateView.as_view(), # views.SubscriptionCreateOrUpdateView.as_view(),
@@ -31,11 +32,11 @@ urlpatterns = [
views.StripeProductCreateOrUpdateView.as_view(), views.StripeProductCreateOrUpdateView.as_view(),
name="stripe_product_add", name="stripe_product_add",
), ),
path( # path(
"product/delete/<int:pk>", # "product/delete/<int:pk>",
views.StripeProductDeleteView.as_view(), # views.StripeProductDeleteView.as_view(),
name="stripe_product_delete", # name="stripe_product_delete",
), # ),
# PLANS # PLANS
path("plan/list/", views.PlanView.as_view(), name="plan_list"), path("plan/list/", views.PlanView.as_view(), name="plan_list"),
# path( # path(

View File

@@ -21,7 +21,13 @@ from manage_wallets.models import (
TransactionStatus, TransactionStatus,
TransactionType, TransactionType,
) )
from .models import Plan, StripeProduct, Subscription, PrincipalSubscription from .models import (
Plan,
StripeProduct,
Subscription,
PrincipalSubscription,
SubscriptionStatus,
)
from django.views import generic from django.views import generic
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
@@ -179,6 +185,20 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView):
return context 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): class SubscriptionDeleteView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
@@ -325,44 +345,61 @@ class StripeProductView(LoginRequiredMixin, generic.ListView):
return context return context
class StripeProductDeleteView(LoginRequiredMixin, generic.View): """ we are not using product delete functionality because there may be multiple stripe's prices
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS attached to one product and in case of any error it will mismatch the stripe's price with
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS our database Subscription objects"""
action = resource_action.ACTION_DELETE # class StripeProductDeleteView(LoginRequiredMixin, generic.View):
model = StripeProduct # page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
success_url = reverse_lazy("manage_subscriptions:stripe_product_list") # resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
success_message = constants.RECORD_DELETED # action = resource_action.ACTION_DELETE
error_message = constants.RECORD_NOT_FOUND # 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): # def get(self, request, pk):
try: # try:
# Retrieve the subscription object # # Retrieve the subscription object
product = self.model.objects.get(id=pk) # product = self.model.objects.get(id=pk)
# Checking if there is a Stripe Product ID associated with the subscription # # Fetching the related subscriptions (prices)
stripe_product_id = product.product_id # related_subscriptions = Subscription.objects.filter(stripe_product=product)
if stripe_product_id:
stripe.api_key = settings.STRIPE_SECRET_KEY
try: # # Checking if there is a Stripe Product ID associated with the subscription
# Updating the Stripe price to mark it as inactive # stripe_product_id = product.product_id
stripe.Product.modify(stripe_product_id, active=False) # if stripe_product_id:
except stripe.error.StripeError as e: # stripe.api_key = settings.STRIPE_SECRET_KEY
# Handle Stripe errors
messages.error(request, f"Stripe error: {str(e)}")
return redirect(self.success_url)
# Updating the subscription model record # # Deactivating related prices on Stripe first
product.deleted = True # for subscription in related_subscriptions:
product.active = False # price_id = subscription.price_id
product.save() # 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: # # Updating the subscription model record
messages.error(request, self.error_message) # 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): # class PlanCreateOrUpdateView(LoginRequiredMixin, generic.View):
@@ -572,6 +609,32 @@ class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View):
class SubscriptionPageView(TemplateView): class SubscriptionPageView(TemplateView):
template_name = "stripe_html/index.html" 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): def get(self, request, *args, **kwargs):
# Example of extracting the token from a query parameter or cookie # Example of extracting the token from a query parameter or cookie
token = request.GET.get("token") or request.session.get("jwt") token = request.GET.get("token") or request.session.get("jwt")
@@ -608,22 +671,25 @@ class SubscriptionPageView(TemplateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
request = self.request request = self.request
if request.user.is_authenticated: if request.user.is_authenticated:
print("request.user: ", request.user) active_subscription = (
subscriptions = Subscription.objects.filter( PrincipalSubscription.objects.filter(
principal_types=request.user.principal_type, principal=request.user,
active=True, is_paid=True,
deleted=False, cancelled=False,
is_free=False, deleted=False,
active=True,
status=SubscriptionStatus.ACTIVE,
)
.order_by("-grace_period_end_date")
.first()
) )
if subscriptions.exists(): if active_subscription:
context["subscriptions"] = subscriptions context["active_subscription"] = active_subscription
context["stripeCheckoutUrl"] = settings.STRIPE_CHECKOUT_URL
context["stripeFinalUrl"] = settings.STRIPE_FINAL_URL
context["couponValidityCheckUrl"] = settings.COUPON_VALIDITY_CHECK_URL
else: else:
# Handling the case where no subscriptions are found for the principal type. # If no active subscription is found, redirect to the SubscriptionPageView
context["error"] = "No subscriptions found for your user type." return redirect("manage_subscriptions:stripe")
return context return context
@@ -691,7 +757,10 @@ def validate_coupon(request):
return JsonResponse( return JsonResponse(
{"error": "Coupon max redeems reached."}, status=400 {"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: except stripe.error.InvalidRequestError:
return JsonResponse( return JsonResponse(
{"error": f"Invalid coupon code: {coupon_code}"}, status=400 {"error": f"Invalid coupon code: {coupon_code}"}, status=400

View File

@@ -49,9 +49,6 @@
style="width: 69.2656px;"> Stripe Product ID </th> style="width: 69.2656px;"> Stripe Product ID </th>
<th class="sorting" tabindex="7" aria-controls="style-3" <th class="sorting" tabindex="7" aria-controls="style-3"
style="width: 79.7969px;">Active</th> style="width: 79.7969px;">Active</th>
<th class="dt-no-sorting sorting" tabindex="8"
aria-controls="style-3"
style="width: 100.625px;">Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -63,29 +60,9 @@
<td class="text-center"> <td class="text-center">
<span class="shadow-none badge {% if data_obj.active %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.active}}</span> <span class="shadow-none badge {% if data_obj.active %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.active}}</span>
</td> </td>
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_subscriptions:stripe_product_delete' data_obj.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Edit" data-bs-original-title="Edit"
aria-label="Delete"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-trash p-1 br-8 mb-1">
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
</path>
</svg>
</a>
</li>
</ul>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -0,0 +1,77 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block content %}
<div class="container mt-5">
<div class="row">
<!-- Subscription Title and Image -->
<div class="col-md-12 mb-4">
<div class="card shadow-sm">
<div class="card-body text-center">
{% if subscription.image %}
<img src="{{ subscription.image.url }}" class="img-fluid rounded mb-3" alt="{{ subscription.title }}" style="max-height: 200px;">
{% endif %}
<h3 class="card-title">{{ subscription.title }}</h3>
</div>
</div>
</div>
<!-- Plan and Pricing Info -->
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header text-white">
<h5 class="card-title"><i class="bi bi-currency-dollar"></i> Plan & Pricing</h5>
</div>
<div class="card-body">
<p><strong>Plan:</strong> {{ subscription.plan.title }}</p>
<p><strong>Price ID:</strong> {{ subscription.price_id|default:"Not a Stripe Subscription" }}</p>
<p><strong>Stripe Product:</strong> {{ subscription.stripe_product|default:"None" }}</p>
<p><strong>Amount:</strong> ${{ subscription.amount }}</p>
<p><strong>High Amount:</strong> ${{ subscription.high_amount }}</p>
<p><strong>Referral Percentage:</strong> {{ subscription.referral_percentage }}%</p>
<p><strong>Is Free:</strong>
{% if subscription.is_free %}
<span class="badge bg-success">Yes</span>
{% else %}
<span class="badge bg-danger">No</span>
{% endif %}
</p>
</div>
</div>
</div>
<!-- Descriptions -->
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header text-dark">
<h5 class="card-title"><i class="bi bi-file-earmark-text"></i> Descriptions</h5>
</div>
<div class="card-body">
<p><strong>Short Description:</strong> {{ subscription.short_description|default:"Not Provided" }}</p>
<p><strong>Long Description:</strong></p>
<p>{{ subscription.long_description|default:"Not Provided" }}</p>
</div>
</div>
</div>
<!-- Principal Types -->
<div class="col-md-12 mb-4">
<div class="card shadow-sm">
<div class="card-header text-white">
<h5 class="card-title"><i class="bi bi-people"></i> Principal Types</h5>
</div>
<div class="card-body">
<ul class="list-group">
{% for principal_type in subscription.principal_types.all %}
<li class="list-group-item">{{ principal_type.name }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -115,7 +115,13 @@
</svg> </svg>
</a> </a>
</li> </li>
<li>
<a href="{% url 'manage_subscriptions:subscription_detail' data_obj.id %}">
<span class="material-symbols-outlined">
visibility
</span>
</a>
</li>
</ul> </ul>
</td> </td>
</tr> </tr>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Active Subscription</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-5">
<div class="card">
<div class="card-header text-center bg-primary text-white">
<h3>Your Active Subscription</h3>
</div>
<div class="card-body">
<!-- Subscription Details -->
<div class="row">
<div class="col-md-6">
<h5>Subscription:</h5>
<p>{{ active_subscription.subscription.name }}</p>
</div>
<div class="col-md-6">
<h5>Principal:</h5>
<p>{{ active_subscription.principal.first_name }} {{ active_subscription.principal.last_name }}</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h5>Status:</h5>
<p>{{ active_subscription.get_status_display }}</p>
</div>
<div class="col-md-6">
<h5>Start Date:</h5>
<p>{{ active_subscription.start_date }}</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h5>End Date:</h5>
<p>{{ active_subscription.end_date }}</p>
</div>
<div class="col-md-6">
<h5>Auto Renew:</h5>
<p>{{ active_subscription.auto_renew|yesno:"Yes,No" }}</p>
</div>
</div>
<!-- Payment Details -->
<div class="row">
<div class="col-md-6">
<h5>Coupon Code:</h5>
<p>{{ active_subscription.coupon_code }}</p>
</div>
</div>
<!-- Stripe Links -->
<div class="text-center mt-4">
<a href="" class="btn btn-success me-2">Cancel Subscription</a>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS (optional) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>