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,
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,

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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")

View File

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

View File

@@ -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

View File

@@ -49,9 +49,6 @@
style="width: 69.2656px;"> Stripe Product ID </th>
<th class="sorting" tabindex="7" aria-controls="style-3"
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>
</thead>
<tbody>
@@ -63,29 +60,9 @@
<td class="text-center">
<span class="shadow-none badge {% if data_obj.active %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.active}}</span>
</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>
{% endfor %}
</tbody>
</table>
</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>
</a>
</li>
<li>
<a href="{% url 'manage_subscriptions:subscription_detail' data_obj.id %}">
<span class="material-symbols-outlined">
visibility
</span>
</a>
</li>
</ul>
</td>
</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>