my subscriptions page
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
77
templates/manage_subscriptions/subscription_details.html
Normal file
77
templates/manage_subscriptions/subscription_details.html
Normal 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 %}
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
70
templates/stripe_html/active_subscription.html
Normal file
70
templates/stripe_html/active_subscription.html
Normal 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>
|
||||||
Reference in New Issue
Block a user