auto recurring testing
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,7 +8,6 @@ class CouponForm(forms.ModelForm):
|
||||
model = Coupon
|
||||
fields = [
|
||||
"title",
|
||||
"coupon_code",
|
||||
"description",
|
||||
"image",
|
||||
"discount_amount",
|
||||
|
||||
18
manage_coupons/migrations/0002_coupon_coupon_id.py
Normal file
18
manage_coupons/migrations/0002_coupon_coupon_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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
47
manage_coupons/utils.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}),
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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)}"
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
49
templates/manage_subscriptions/product_add.html
Normal file
49
templates/manage_subscriptions/product_add.html
Normal 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 %}
|
||||
124
templates/manage_subscriptions/product_list.html
Normal file
124
templates/manage_subscriptions/product_list.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user