auto recurring testing

This commit is contained in:
rizwanisready
2024-07-31 13:12:17 +05:30
parent 7992e456a4
commit e3189344ea
18 changed files with 704 additions and 69 deletions

View File

@@ -303,8 +303,11 @@ SIMPLE_JWT = {
"JTI_CLAIM": "jti",
}
STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY")
STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY")
STRIPE_SECRET_KEY = "sk_test_51OexsKCesU6kunsIsbSKSZc1BF4gjklniaue8lmpkGKqDzenQtMkR8tKAryxErJXqp0jPiu1Gg7papa4tqZfKL9G00qUM4toB2"
STRIPE_PUBLISH_KEY = "pk_test_51OexsKCesU6kunsINDvKUhbelxeUmDAVZGSOisZ6XXHCp3pKtl4vs0pR42w0OcjZhngmECsXQNbAKNPOhiFMTJ8o00sRZQG0lh"
# STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY")
# STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY")
ONE_SIGNAL_APP_ID = env.str("ONE_SIGNAL_APP_ID")

View File

@@ -1,3 +1,4 @@
import datetime
from django.conf import settings
from django.db import transaction
from django.shortcuts import get_object_or_404
@@ -130,6 +131,13 @@ class WebhookService:
logger.error(f"Invalid subscription ID: {subscription_id}")
raise ValueError(f"Invalid subscription ID: {subscription_id}")
def get_stripe_subscription(self):
stripe_subscription_id = self.charge_data["metadata"]["subscription"]
if stripe_subscription_id:
return stripe_subscription_id
else:
return None
def get_order_id(self):
return self.charge_data["metadata"]["order_id"]
@@ -236,23 +244,44 @@ class SubscriptionService:
def __init__(self):
self.principal_subscription = None
def create_principal_subscription(self, principal, subscription, order_id, coupon=None):
def create_principal_subscription(
self,
principal,
subscription,
stripe_subscription,
order_id,
current_period_start,
current_period_end,
coupon=None,
):
subscription_days = subscription.plan.days
today = timezone.now().date()
last_date = today + timedelta(days=subscription_days)
start_date = (
datetime.datetime.fromtimestamp(current_period_start).date()
if current_period_start
else today
)
end_date = (
datetime.datetime.fromtimestamp(current_period_end).date()
if current_period_end
else (today + timedelta(days=subscription_days))
)
principal_subscription = PrincipalSubscription.objects.create(
principal=principal,
subscription=subscription,
stripe_subscription_id=stripe_subscription if stripe_subscription else "",
is_paid=True,
is_stripe_subscription=True if stripe_subscription else False,
order_id=order_id,
start_date=today,
end_date=last_date,
grace_period_end_date=last_date + timedelta(days=15),
start_date=start_date,
end_date=end_date,
grace_period_end_date=end_date + timedelta(days=15),
coupon_code=coupon.coupon_code if coupon else None,
)
if coupon:
coupon.no_of_redeems += 1
coupon.save()
# if coupon:
# coupon.no_of_redeems += 1
# coupon.save()
self.principal_subscription = principal_subscription
return principal_subscription
@@ -267,13 +296,16 @@ class SubscriptionService:
class PaymentProcessingService:
def __init__(self, webhook_data):
def __init__(self, webhook_data, current_period_start, current_period_end):
self.webhook_service = WebhookService(webhook_data)
self.notification_service = NotificationService()
self.current_period_start = current_period_start
self.current_period_end = current_period_end
# Retrieve objects
self.principal = self.webhook_service.get_principal()
self.transaction = self.webhook_service.get_transaction()
self.subscription = self.webhook_service.get_subscription()
self.stripe_subscription = self.webhook_service.get_stripe_subscription()
self.order_id = self.webhook_service.get_order_id()
self.coupon = self.webhook_service.get_coupon()
self.subscription_service = SubscriptionService()
@@ -290,7 +322,13 @@ class PaymentProcessingService:
# Create or update the principal subscription
self.principal_subscription = (
self.subscription_service.create_principal_subscription(
self.principal, self.subscription, self.order_id, self.coupon
self.principal,
self.subscription,
self.stripe_subscription,
self.order_id,
self.coupon,
self.current_period_start,
self.current_period_end,
)
)
print("First Part Done....!!!!!")
@@ -303,9 +341,9 @@ class PaymentProcessingService:
referral_service = ReferralRewardService(
self.principal, self.principal_subscription, self.subscription
)
print("Above Third Part...!!!!!!!!!!!")
print("Third Part Done...!!!!!!!!!!!")
referral_service.credit_referral_reward_if_applicable()
print("Third Part Done....!!!!!")
print("Fourth Part Done....!!!!!")
self.notification_service.payment_success_notification(
self.principal,
self.subscription,

View File

@@ -8,7 +8,6 @@ class CouponForm(forms.ModelForm):
model = Coupon
fields = [
"title",
"coupon_code",
"description",
"image",
"discount_amount",

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-07-31 07:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_coupons", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="coupon",
name="coupon_id",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -6,6 +6,7 @@ from accounts.models import BaseModel, IAmPrincipalType
class Coupon(BaseModel):
title = models.CharField(max_length=255)
coupon_code = models.CharField(max_length=50, unique=True)
coupon_id = models.CharField(max_length=255, blank=True, null=True)
no_of_redeems = models.IntegerField(default=0)
description = models.TextField(null=True, blank=True)
image = models.ImageField(upload_to="coupon_img", null=True, blank=True)

View File

@@ -10,11 +10,11 @@ urlpatterns = [
views.CouponCreateOrUpdateView.as_view(),
name="coupon_add",
),
path(
"coupon/edit/<int:pk>/",
views.CouponCreateOrUpdateView.as_view(),
name="coupon_edit",
),
# path(
# "coupon/edit/<int:pk>/",
# views.CouponCreateOrUpdateView.as_view(),
# name="coupon_edit",
# ),
path(
"coupon/delete/<int:pk>/",
views.CouponDeleteView.as_view(),

47
manage_coupons/utils.py Normal file
View File

@@ -0,0 +1,47 @@
import stripe
from decimal import Decimal
def handle_stripe_coupon(coupon_instance, stripe_secret_key):
"""
Handles the creation or updating of a Stripe coupon.
Returns True if successful, otherwise returns False.
"""
try:
stripe.api_key = stripe_secret_key
# Prepare coupon data without setting the ID
coupon_data = {
"name": coupon_instance.title,
"metadata": {
"local_id": coupon_instance.id,
},
"redeem_by": int(coupon_instance.valid_to.timestamp()),
"max_redemptions": (
coupon_instance.max_redeems if coupon_instance.max_redeems > 0 else None
),
"duration": "once",
}
if coupon_instance.discount_amount:
coupon_data["amount_off"] = int(
coupon_instance.discount_amount * Decimal(100)
) # Amount in cents/fils
coupon_data["currency"] = "gbp"
elif coupon_instance.discount_percentage:
coupon_data["percent_off"] = float(coupon_instance.discount_percentage)
# Creating a new Stripe coupon
stripe_coupon = stripe.Coupon.create(**coupon_data)
# Using the Stripe-generated ID for coupon_code and coupon_id
coupon_instance.coupon_code = stripe_coupon.id
coupon_instance.coupon_id = stripe_coupon.id
# Saving the coupon instance after successful Stripe operation
coupon_instance.save()
return True, "Coupon successfully created or updated."
except Exception as e:
error_message = f"Error creating/updating Stripe coupon: {e}"
print(error_message)
return False, error_message

View File

@@ -1,12 +1,15 @@
from django.conf import settings
from django.shortcuts import get_object_or_404, render, redirect
from django.views import generic
from django.contrib.auth.mixins import LoginRequiredMixin
import stripe
from accounts import resource_action
from django.urls import reverse_lazy
from django.contrib import messages
from goodtimes import constants
from manage_coupons.forms import CouponForm
from manage_coupons.models import Coupon
from manage_coupons.utils import handle_stripe_coupon
# Create your views here.
@@ -87,9 +90,18 @@ class CouponCreateOrUpdateView(LoginRequiredMixin, generic.View):
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)
success, message = handle_stripe_coupon(
form.instance, settings.STRIPE_SECRET_KEY
)
if success:
messages.success(self.request, message)
return redirect(self.success_url)
else:
messages.error(self.request, message)
return render(
request, self.template_name, context=self.get_context_data(form=form)
)
class CouponDeleteView(LoginRequiredMixin, generic.View):
@@ -104,11 +116,22 @@ class CouponDeleteView(LoginRequiredMixin, generic.View):
def get(self, request, pk):
try:
type_obj = self.model.objects.get(id=pk)
if type_obj.coupon_id:
stripe.api_key = settings.STRIPE_SECRET_KEY
try:
stripe.Coupon.delete(type_obj.coupon_id)
except stripe.error.StripeError as e:
# Handle Stripe errors
error_message = f"Stripe error: {e.user_message or e}"
messages.error(request, error_message)
return redirect(self.success_url)
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)
messages.warning(request, self.error_message)
return redirect(self.success_url)

View File

@@ -179,6 +179,9 @@ class StripeWebhookTest(APIView):
event_id = event["id"]
event_type = event["type"]
principal_id = event["data"]["object"]["metadata"]["principal"]
stripe_subscription_id = stripe.Subscription.retrieve(
event["data"]["object"]["metadata"]["subscription"]
)
webhook_event, created = WebhookEvent.objects.get_or_create(
event_id=event_id,
@@ -188,21 +191,25 @@ class StripeWebhookTest(APIView):
},
)
if stripe_subscription_id:
stripe_subscription = stripe.Subscription.retrieve(stripe_subscription_id)
current_period_start = stripe_subscription["current_period_start"]
current_period_end = stripe_subscription["current_period_end"]
else:
current_period_start = None
current_period_end = None
if not created and webhook_event.status == "processed":
return ApiResponse.success(
status=status.HTTP_208_ALREADY_REPORTED,
message="Event already processed",
)
# Check if there is an active principal subscription
# if self._has_active_principal_subscription(principal_id):
# return ApiResponse.success(
# status=status.HTTP_208_ALREADY_REPORTED,
# message="Active principal subscription already exists",
# )
# payment_service = services.PaymentProcessingService(webhook_data=event)
payment_service = PaymentProcessingService(webhook_data=event)
payment_service = PaymentProcessingService(
webhook_data=event,
current_period_start=current_period_start,
current_period_end=current_period_end,
)
payment_service.process_event()
webhook_event = WebhookEvent.objects.get(event_id=event_id)
webhook_event.status = "processed"

View File

@@ -1,6 +1,11 @@
from django import forms
from accounts.models import IAmPrincipalType
from manage_subscriptions.models import PrincipalSubscription, Subscription, Plan
from manage_subscriptions.models import (
PrincipalSubscription,
StripeProduct,
Subscription,
Plan,
)
class PlanForm(forms.ModelForm):
@@ -53,3 +58,15 @@ class PrincipalSubscriptionForm(forms.ModelForm):
"grace_period_end_date": forms.DateInput(attrs={"type": "date"}),
"cancelled_date_time": forms.DateTimeInput(attrs={"type": "datetime"}),
}
class StripeProductForm(forms.ModelForm):
class Meta:
model = StripeProduct
fields = [
"title",
"description",
]
widgets = {
"description": forms.Textarea(attrs={"rows": 3}),
}

View File

@@ -0,0 +1,97 @@
# Generated by Django 5.0.2 on 2024-07-31 07:34
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_subscriptions", "0009_principalsubscription_coupon_code"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="principalsubscription",
name="comments",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="principalsubscription",
name="is_stripe_subscription",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="principalsubscription",
name="stripe_subscription_id",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="subscription",
name="price_id",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.CreateModel(
name="StripeProduct",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("active", models.BooleanField(default=True)),
("deleted", models.BooleanField(default=False)),
("created_on", models.DateTimeField(auto_now_add=True)),
("modified_on", models.DateTimeField(auto_now=True)),
("title", models.CharField(max_length=255)),
("product_id", models.CharField(blank=True, max_length=255, null=True)),
("description", models.TextField(blank=True, null=True)),
("metadata", models.JSONField(blank=True, null=True)),
("image_url", models.URLField(blank=True, null=True)),
(
"default_price_id",
models.CharField(blank=True, max_length=255, null=True),
),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_created",
to=settings.AUTH_USER_MODEL,
),
),
(
"modified_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_modified",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "stripe_product",
},
),
migrations.AddField(
model_name="subscription",
name="stripe_product",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="subscription_product",
to="manage_subscriptions.stripeproduct",
),
),
]

View File

@@ -17,8 +17,31 @@ class Plan(BaseModel):
return self.title
class StripeProduct(BaseModel):
title = models.CharField(max_length=255)
product_id = models.CharField(max_length=255, blank=True, null=True)
description = models.TextField(blank=True, null=True)
metadata = models.JSONField(blank=True, null=True)
image_url = models.URLField(blank=True, null=True)
default_price_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
db_table = "stripe_product"
def __str__(self):
return self.title
class Subscription(BaseModel):
title = models.CharField(max_length=255)
price_id = models.CharField(max_length=255, blank=True, null=True)
stripe_product = models.ForeignKey(
StripeProduct,
related_name="subscription_product",
on_delete=models.CASCADE,
null=True,
blank=True,
)
short_description = models.CharField(max_length=255, null=True, blank=True)
long_description = models.TextField(null=True, blank=True)
image = models.ImageField(upload_to="subscription_img", null=True, blank=True)
@@ -31,7 +54,10 @@ class Subscription(BaseModel):
IAmPrincipalType, related_name="principal_type_subscriptions", blank=True
)
referral_percentage = models.DecimalField(max_digits=5, decimal_places=2)
is_free = models.BooleanField(default=False, help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.")
is_free = models.BooleanField(
default=False,
help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.",
)
class Meta:
db_table = "subscription"
@@ -67,6 +93,9 @@ class PrincipalSubscription(BaseModel):
cancelled_date_time = models.DateTimeField(null=True, blank=True)
grace_period_end_date = models.DateField(null=True, blank=True)
stripe_customer_id = models.CharField(max_length=255, null=True, blank=True)
stripe_subscription_id = models.CharField(max_length=255, null=True, blank=True)
comments = models.CharField(max_length=255, null=True, blank=True)
is_stripe_subscription = models.BooleanField(default=False)
payment_intent_id = models.CharField(max_length=255, null=True, blank=True)
payment_intent_client_secret = models.CharField(
max_length=255, null=True, blank=True
@@ -78,7 +107,7 @@ class PrincipalSubscription(BaseModel):
def __str__(self):
return f"{self.subscription} - {self.principal.first_name}"
def generate_order_id(email):
return f"order_{str(timezone.localtime().timestamp())}{str(email)}"

View File

@@ -12,16 +12,16 @@ urlpatterns = [
views.SubscriptionCreateOrUpdateView.as_view(),
name="subscription_add",
),
path(
"subscription/edit/<int:pk>/",
views.SubscriptionCreateOrUpdateView.as_view(),
name="subscription_edit",
),
# path(
# "subscription/delete/<int:pk>",
# views.SubscriptionDeleteView.as_view(),
# name="subscription_delete",
# "subscription/edit/<int:pk>/",
# views.SubscriptionCreateOrUpdateView.as_view(),
# name="subscription_edit",
# ),
path(
"subscription/delete/<int:pk>",
views.SubscriptionDeleteView.as_view(),
name="subscription_delete",
),
# PLANS
path("plan/list/", views.PlanView.as_view(), name="plan_list"),
# path(

View File

@@ -11,6 +11,7 @@ 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,
)
@@ -20,7 +21,7 @@ from manage_wallets.models import (
TransactionStatus,
TransactionType,
)
from .models import Plan, Subscription, PrincipalSubscription
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
@@ -104,10 +105,52 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View):
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
@@ -132,26 +175,165 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView):
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
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:
# 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)
def get(self, request, pk):
try:
# Retrieve the subscription object
subscription = self.model.objects.get(id=pk)
# return redirect(self.success_url)
# 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: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)
form.save()
messages.success(self.request, self.get_success_message())
return redirect(self.success_url)
class StripeProductView(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/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 = Subscription
success_url = reverse_lazy("manage_subscriptions: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):
@@ -419,7 +601,7 @@ class SubscriptionPageView(TemplateView):
@csrf_exempt
def stripe_config(request):
if request.method == "GET":
stripe_config = {"publicKey": settings.STRIPE_TEST_MODE_PUBLISH_KEY}
stripe_config = {"publicKey": settings.STRIPE_PUBLISH_KEY}
return JsonResponse(stripe_config, safe=False)
@@ -468,7 +650,7 @@ def validate_coupon(request):
@require_POST
def create_checkout_session(request):
success_url = reverse_lazy("manage_subscriptions:stripe")
stripe.api_key = settings.STRIPE_TEST_MODE_SECRET_KEY
stripe.api_key = settings.STRIPE_SECRET_KEY
data = json.loads(request.body)
print("data: ", data)
subscription_id = data.get("subscriptionId", None)

View File

@@ -88,7 +88,7 @@
</td>
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_coupons:coupon_edit' data_obj.id %}" class="bs-tooltip"
<!-- <li><a href="{% url 'manage_coupons:coupon_edit' 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="Edit"><svg xmlns="http://www.w3.org/2000/svg"
@@ -101,7 +101,7 @@
</path>
</svg>
</a>
</li>
</li> -->
<li><a href="{% url 'manage_coupons:coupon_delete' data_obj.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Delete" data-bs-original-title="Delete"

View File

@@ -0,0 +1,49 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
{% include "cdn_through_html/filepond_cdn_css.html" %}
{% include "cdn_through_html/quill_cdn_css.html" %}
{% include "cdn_through_html/tagify_cdn_css.html" %}
{{form.media}}
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>Add Product</h3>
</div>
<div class="col text-end">
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="POST" novalidate>
{% csrf_token %}
{% include 'includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,124 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
{% include "cdn_through_html/datatable_cdn_css.html" %}
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row">
<div class="col-sm-6">
<h3>Manage Products</h3>
</div>
<div class="col-sm-6 text-md-end">
<!--
<button class="btn btn-dark mb-2 me-md-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button>
-->
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:product_add' %}">Add Products</a>
<!-- <a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:plan_list' %}">Plans</a>
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:principal_subscriptions_list' %}">Principal Subscription</a> -->
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<div id="style-3_wrapper" class="dataTables_wrapper container-fluid dt-bootstrap4 no-footer">
<div class="table-responsive">
<table id="style-3" class="table style-3 dt-table-hover dataTable no-footer" role="grid"
aria-describedby="style-3_info">
<thead>
<tr role="row">
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Record Id </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Title </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
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>
{% for data_obj in subscription_obj %}
<tr role="row">
<td class="checkbox-column text-center sorting_1"> {{data_obj.id}}</td>
<td>{{data_obj.title}}</td>
<td>{{data_obj.product_id}}</td>
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_subscriptions: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-edit-2 p-1 br-8 mb-1">
<path
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
</path>
</svg>
</a>
</li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
{% include "cdn_through_html/datatable_cdn_js.html" %}
<script>
c3 = $('#style-3').DataTable({
"dom": "<'dt--top-section'<'row'<'col-12 col-sm-6 d-flex justify-content-sm-start justify-content-center'l><'col-12 col-sm-6 d-flex justify-content-sm-end justify-content-center mt-sm-0 mt-3'f>>>" +
"<'table-responsive'tr>" +
"<'dt--bottom-section d-sm-flex justify-content-sm-between text-center'<'dt--pages-count mb-sm-0 mb-3'i><'dt--pagination'p>>",
"oLanguage": {
"oPaginate": { "sPrevious": '<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-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>', "sNext": '<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-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' },
"sInfo": "Showing page _PAGE_ of _PAGES_",
"sSearch": '<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-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
"sSearchPlaceholder": "Search...",
"sLengthMenu": "Results : _MENU_",
},
"order": [[ 0, "desc" ]],
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 10
});
multiCheck(c3);
</script>
{% endblock %}

View File

@@ -88,10 +88,10 @@
</td>
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_subscriptions:subscription_edit' data_obj.id %}" class="bs-tooltip"
<li><a href="{% url 'manage_subscriptions:subscription_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="Edit"><svg xmlns="http://www.w3.org/2000/svg"
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"
@@ -102,6 +102,7 @@
</svg>
</a>
</li>
</ul>
</td>
</tr>