refactor(subscription): removed unnecessary field and complexity
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import random
|
||||
import requests
|
||||
import googlemaps
|
||||
import stripe
|
||||
import stripe.error
|
||||
import tweepy
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
@@ -13,12 +15,7 @@ from django.db.models import Case, When
|
||||
from smtplib import SMTPException
|
||||
|
||||
from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType
|
||||
from manage_referrals.models import (
|
||||
GoodTimeCoins,
|
||||
ReferralRecord,
|
||||
ReferralRecordReward,
|
||||
ReferralTracking,
|
||||
)
|
||||
|
||||
from manage_subscriptions.models import PrincipalSubscription, Subscription
|
||||
from manage_wallets.models import (
|
||||
TransactionStatus,
|
||||
@@ -208,195 +205,6 @@ class SMSService:
|
||||
# self.send(phone_numbers, body)
|
||||
return otp_code
|
||||
|
||||
|
||||
class PaymentProcessingService:
|
||||
def __init__(self, webhook_data):
|
||||
self.webhook_data = webhook_data
|
||||
self.event_type = webhook_data["type"]
|
||||
self.charge_data = webhook_data["data"]["object"]
|
||||
self.customer_id = self._get_customer_id()
|
||||
self.transaction = self._get_transaction_by_id()
|
||||
self.principal = self.transaction.principal
|
||||
self.principal_subscription = None
|
||||
|
||||
def _get_customer_id(self):
|
||||
# Access the customer ID from the charge object
|
||||
return self.charge_data.get("customer", None)
|
||||
|
||||
def _get_transaction_by_id(self):
|
||||
logger.debug("self.metadata: ", self.charge_data["metadata"])
|
||||
logger.debug("transaction_id: ", self.charge_data["metadata"]["transaction_id"])
|
||||
transaction_id = self.charge_data["metadata"]["transaction_id"]
|
||||
if transaction_id:
|
||||
try:
|
||||
logger.debug("_get_transaction_by_id: ", transaction_id)
|
||||
return Transaction.objects.get(id=int(transaction_id))
|
||||
except Transaction.DoesNotExist:
|
||||
logger.error(f"Transaction ID {transaction_id} not found.")
|
||||
return None
|
||||
|
||||
def _get_subscription(self):
|
||||
logger.debug(
|
||||
"subscription_id: ", self.charge_data["metadata"]["subscription_id"]
|
||||
)
|
||||
subscription_id = self.charge_data["metadata"]["subscription_id"]
|
||||
if subscription_id:
|
||||
try:
|
||||
return Subscription.objects.get(id=int(subscription_id))
|
||||
except Subscription.DoesNotExist:
|
||||
logger.error(f"Subscription ID {subscription_id} not found.")
|
||||
return None
|
||||
|
||||
def _create_principal_subscription(self):
|
||||
order_id = self.charge_data["metadata"]["order_id"]
|
||||
try:
|
||||
subscription = self._get_subscription()
|
||||
|
||||
subscription_days = subscription.plan.days
|
||||
today = timezone.now().date()
|
||||
last_date = today + timedelta(days=int(subscription_days))
|
||||
|
||||
principal_subscription = PrincipalSubscription.objects.create(
|
||||
principal=self.principal,
|
||||
subscription=subscription,
|
||||
is_paid=True,
|
||||
order_id=order_id,
|
||||
start_date=today,
|
||||
end_date=last_date,
|
||||
grace_period_end_date=last_date + timedelta(days=15),
|
||||
)
|
||||
self.principal_subscription = principal_subscription
|
||||
return principal_subscription
|
||||
except Subscription.DoesNotExist:
|
||||
logger.error(
|
||||
"SOmething Went Wrong inside _create_principal_subscription()."
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def process_event(self):
|
||||
if self.event_type == "checkout.session.completed":
|
||||
self._handle_success()
|
||||
else:
|
||||
self._handle_failure()
|
||||
|
||||
def _handle_success(self):
|
||||
with transaction.atomic():
|
||||
self._create_principal_subscription()
|
||||
self._update_transaction_success()
|
||||
self._credit_referral_reward_if_applicable()
|
||||
|
||||
def _credit_referral_reward_if_applicable(self):
|
||||
# Step 1: Check for an existing, completed referral record
|
||||
referral_record = ReferralRecord.objects.filter(
|
||||
active=True,
|
||||
deleted=False,
|
||||
referred_principal_id=self.principal.id,
|
||||
is_completed=True,
|
||||
).first()
|
||||
|
||||
if referral_record:
|
||||
# Step 2: Check for an active subscription of the referrer
|
||||
today = timezone.now().date()
|
||||
active_subscription = (
|
||||
PrincipalSubscription.objects.filter(
|
||||
principal=referral_record.referrer_principal,
|
||||
is_paid=True,
|
||||
end_date__gte=today,
|
||||
cancelled=False,
|
||||
deleted=False,
|
||||
)
|
||||
.order_by("-end_date")
|
||||
.first()
|
||||
)
|
||||
if active_subscription:
|
||||
subscription = self._get_subscription()
|
||||
if subscription:
|
||||
# Calculate the reward value
|
||||
percentage = (
|
||||
subscription.referral_percentage * subscription.amount / 100
|
||||
)
|
||||
|
||||
# Create a reward entry
|
||||
ReferralRecordReward.objects.create(
|
||||
referral_record=referral_record,
|
||||
subscription=subscription,
|
||||
coins=1, # Assuming this is a default or a calculated value
|
||||
value=percentage,
|
||||
)
|
||||
|
||||
self._credit_good_time_coin(
|
||||
referral_record.referrer_principal, percentage
|
||||
)
|
||||
# Here's where you call _update_reward
|
||||
self._update_reward(
|
||||
referral_record=referral_record,
|
||||
active_subscription=active_subscription,
|
||||
create_subscription_method=self.principal_subscription,
|
||||
has_active_subscription=True,
|
||||
)
|
||||
else:
|
||||
# If there is no active subscription, still need to update reward without active_subscription
|
||||
self._update_reward(
|
||||
referral_record=referral_record,
|
||||
active_subscription=None,
|
||||
create_subscription_method=self.principal_subscription,
|
||||
has_active_subscription=False,
|
||||
)
|
||||
|
||||
def _credit_good_time_coin(self, referrer_principal, percentage):
|
||||
# wallet, created = Wallet.objects.get_or_create(principal=referrer_principal)
|
||||
# wallet.coins += 1
|
||||
# wallet.save()
|
||||
Transaction.objects.create(
|
||||
principal=referrer_principal,
|
||||
transaction_type=TransactionType.CREDIT,
|
||||
payment_method="",
|
||||
transaction_status=TransactionStatus.SUCCESS,
|
||||
amount=percentage,
|
||||
coins=1,
|
||||
comment="Referral reward",
|
||||
# Populate other fields as necessary, such as `order_id`, `product_id`, or `reference_id` if applicable
|
||||
)
|
||||
|
||||
def _handle_failure(self):
|
||||
# Implement any necessary logic to handle a failed payment
|
||||
self._update_transaction_failure()
|
||||
|
||||
def _update_reward(
|
||||
self,
|
||||
referral_record,
|
||||
active_subscription,
|
||||
create_subscription_method,
|
||||
has_active_subscription,
|
||||
):
|
||||
# Check if the referrer has an active subscription and get its ID if it exists
|
||||
referrer_subscription_id = (
|
||||
active_subscription.id if active_subscription else None
|
||||
)
|
||||
|
||||
# Create a new subscription for the referred principal
|
||||
referred_subscription_id = self.principal_subscription.id
|
||||
|
||||
# Create or update the ReferralTracking record
|
||||
ReferralTracking.objects.create(
|
||||
referral_record=referral_record,
|
||||
referrer_subscription_id=referrer_subscription_id,
|
||||
referred_subscription_id=referred_subscription_id,
|
||||
is_referrer_subscribed=has_active_subscription,
|
||||
)
|
||||
|
||||
def _update_transaction_success(self):
|
||||
principal_subscription = self.principal_subscription
|
||||
self.transaction.transaction_status = TransactionStatus.SUCCESS
|
||||
self.transaction.principal_subscription = principal_subscription
|
||||
self.transaction.save()
|
||||
|
||||
def _update_transaction_failure(self):
|
||||
self.transaction.transaction_status = TransactionStatus.FAIL
|
||||
self.transaction.save()
|
||||
|
||||
|
||||
class InteractionCalculator:
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
@@ -1090,3 +898,172 @@ class InstagramPoster:
|
||||
if not result:
|
||||
return {'success': False, 'message': 'Error posting photo in Instagram.'}
|
||||
return {'success': True, 'message': 'Photo posted successfully'}
|
||||
|
||||
|
||||
class StripeService:
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
|
||||
@staticmethod
|
||||
def create_product(name: str, description: str = None, metadata: dict = None):
|
||||
"""
|
||||
Create a Stripe Product.
|
||||
|
||||
:param name: Name of the product, meant to be displayable to the customer.
|
||||
:param description: An optional description of the product.
|
||||
:param metadata: An optional dictionary of key-value pairs to attach to the product.
|
||||
:return: The created Stripe product object.
|
||||
|
||||
See: https://docs.stripe.com/api/products/create?lang=python
|
||||
"""
|
||||
try:
|
||||
product = stripe.Product.create(name=name, description=description, metadata=metadata)
|
||||
return {'success': True, 'data': product}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error creating product: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def retrive_product(product_id: str):
|
||||
"""
|
||||
Retrieve a Stripe Product by its ID.
|
||||
|
||||
:param product_id: The ID of the product to retrieve.
|
||||
:return: The retrieved Stripe Product object.
|
||||
|
||||
See: https://docs.stripe.com/api/products/update?lang=python
|
||||
"""
|
||||
try:
|
||||
product = stripe.Product.retrieve(product_id)
|
||||
return {'success': True, 'data': product}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error retriving product: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def update_product(product_id: str, **kwargs):
|
||||
"""
|
||||
Update a Stripe Product by its ID.
|
||||
|
||||
:param product_id: The ID of the product to update.
|
||||
:param kwargs: Optional paramters to update the product, such as:
|
||||
- name : The new name of the product.
|
||||
- description : The new description of the product.
|
||||
- active : A boolean flag indicating if the product is active.
|
||||
- metadata : A dictionary of key-value pairs to attach to the product.
|
||||
:return: The updated Stripe Product object.
|
||||
|
||||
See: https://docs.stripe.com/api/products/update?lang=python
|
||||
"""
|
||||
try:
|
||||
product = stripe.Product.modify(product_id, **kwargs)
|
||||
return {'success': True, 'data': product}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error updating product: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def delete_product(product_id: str):
|
||||
"""
|
||||
Delete a Stripe Product by its ID.
|
||||
|
||||
:param product_id: ID of the product to delete.
|
||||
:return: The deleted Stripe Product object.
|
||||
|
||||
See: https://docs.stripe.com/api/products/delete?lang=python
|
||||
"""
|
||||
try:
|
||||
product = stripe.Product.delete(product_id)
|
||||
return {'success': True, 'data': product}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error deleting product: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def create_price(product_id: str = None, product_data: dict = None, unit_amount: int = None, currency: str = 'gbp', recurring: dict = None, metadata: dict = None):
|
||||
"""
|
||||
Create a Stripe Price for a product.
|
||||
|
||||
:param product_id: ID of the product for which the price is being created.
|
||||
:param product_data: A dictionary with product details to create a new product on the fly. Example:
|
||||
- name : The name of the product.
|
||||
- description : The description of the product.
|
||||
:param unit_amount: The amount to be charged.(in cents)
|
||||
:param currency: The currency of the price.
|
||||
:param recurring: A dictionary with recurring pricing details. Example:
|
||||
- interval : The interval at which the price is charged (e.g., 'day', 'week', 'month', 'year'.
|
||||
- interval_count : The number of intervals at which the price is charged.
|
||||
:param metadata: An optional dictionary of key-value pairs to attach to the price.
|
||||
:return: The created Stripe Price object.
|
||||
:raise ValueError: If neither product_id nor product_data is provided.
|
||||
|
||||
See: https://docs.stripe.com/api/prices/create?lang=python
|
||||
"""
|
||||
if not product_id and not product_data:
|
||||
raise ValueError("Either product_id or product_data must be provided to create a price.")
|
||||
|
||||
price_data = {
|
||||
'unit_amount': unit_amount,
|
||||
'currency': currency,
|
||||
'recurring': recurring,
|
||||
'metadata': metadata
|
||||
}
|
||||
|
||||
if product_id:
|
||||
price_data['product'] = product_id
|
||||
elif product_data:
|
||||
price_data['product'] = stripe.Product.create(**product_data).id
|
||||
try:
|
||||
price = stripe.Price.create(**price_data)
|
||||
return {'success': True, 'data': price}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error creating price: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def retrieve_price(price_id: str):
|
||||
"""
|
||||
Retrieve a Stripe Price by its ID.
|
||||
:param price_id: ID of the price to retrive
|
||||
:return: The retrieved Stripe Price object
|
||||
|
||||
See: https://docs.stripe.com/api/prices/retrieve?lang=python
|
||||
"""
|
||||
try:
|
||||
price = stripe.Price.retrieve(price_id)
|
||||
return {'success': True, 'data': price}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error retrieving price: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def update_price(price_id: str, **kwargs):
|
||||
"""
|
||||
Update a Stripe Price by its ID.
|
||||
:param price_id: ID of the price to update
|
||||
:param kwargs: Optional parameters to update the price, such as:
|
||||
- active: A boolean flag indicating if the price is active.
|
||||
- nickname: A nickname for the price, useful for labeling and organizing.
|
||||
- metadata: A set of key-value pairs to attach to the price object.
|
||||
:return: The updated Stripe Price object
|
||||
|
||||
See: https://docs.stripe.com/api/prices/update?lang=python
|
||||
"""
|
||||
try:
|
||||
price = stripe.Price.modify(price_id, **kwargs)
|
||||
return {'success': True, 'data': price}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error updating price: {e}"}
|
||||
|
||||
# stipe not provide to delete the price
|
||||
|
||||
@staticmethod
|
||||
def cancel_auto_renew_subscription(subscription_id: str) -> dict:
|
||||
"""
|
||||
Cancels the auto-renewal of a Stripe subscription.
|
||||
|
||||
:param subscription_id: The ID of the subscription to cancel auto-renewal for.
|
||||
:return: A dictionary with success status and the updated subscription object or an error message.
|
||||
"""
|
||||
try:
|
||||
# Update the subscription to cancel at the end of the current period
|
||||
subscription = stripe.Subscription.modify(
|
||||
subscription_id,
|
||||
cancel_at_period_end=True
|
||||
)
|
||||
return {'success': True, 'data': subscription}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f'Error cancelling subscription auto-renewal: {e}'}
|
||||
@@ -51,8 +51,7 @@ STATIC_URL = "/static/"
|
||||
|
||||
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
|
||||
STRIPE_CHECKOUT_URL = "http://localhost:8000/subscriptions/stripe-subscription/"
|
||||
STRIPE_FINAL_URL = "http://localhost:8000/subscriptions/create-checkout-session/"
|
||||
COUPON_VALIDITY_CHECK_URL = "http://localhost:8000/subscriptions/coupon-validity-check/"
|
||||
STRIPE_CHECKOUT_URL = "https://c2f5-122-179-140-110.ngrok-free.app/subscriptions/create-checkout-session/"
|
||||
COUPON_VALIDITY_CHECK_URL = "https://c2f5-122-179-140-110.ngrok-free.app/subscriptions/coupon-validity-check/"
|
||||
|
||||
LOGO_PATH = "static"
|
||||
|
||||
@@ -77,9 +77,6 @@ STATIC_URL = "/static/"
|
||||
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
|
||||
STRIPE_CHECKOUT_URL = (
|
||||
"https://admin.goodtimesltd.co.uk/subscriptions/stripe-subscription/"
|
||||
)
|
||||
STRIPE_FINAL_URL = (
|
||||
"https://admin.goodtimesltd.co.uk/subscriptions/create-checkout-session/"
|
||||
)
|
||||
COUPON_VALIDITY_CHECK_URL = "https://admin.goodtimesltd.co.uk/subscriptions/coupon-validity-check/"
|
||||
|
||||
@@ -77,9 +77,6 @@ STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
|
||||
|
||||
STRIPE_CHECKOUT_URL = (
|
||||
"https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/"
|
||||
)
|
||||
STRIPE_FINAL_URL = (
|
||||
"https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/"
|
||||
)
|
||||
COUPON_VALIDITY_CHECK_URL = "https://staging.goodtimesltd.co.uk/subscriptions/coupon-validity-check/"
|
||||
|
||||
@@ -76,9 +76,6 @@ STATIC_URL = "/static/"
|
||||
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
|
||||
STRIPE_CHECKOUT_URL = (
|
||||
"https://goodtimes.betadelivery.com/subscriptions/stripe-subscription/"
|
||||
)
|
||||
STRIPE_FINAL_URL = (
|
||||
"https://goodtimes.betadelivery.com/subscriptions/create-checkout-session/"
|
||||
)
|
||||
COUPON_VALIDITY_CHECK_URL = (
|
||||
|
||||
@@ -47,8 +47,7 @@ class ReferralRewardService:
|
||||
principal=referrer_principal,
|
||||
is_paid=True,
|
||||
end_date__gte=today,
|
||||
cancelled=False,
|
||||
deleted=False,
|
||||
active=True,
|
||||
)
|
||||
.order_by("-end_date")
|
||||
.first()
|
||||
|
||||
@@ -30,20 +30,19 @@ class SubscriptionService:
|
||||
):
|
||||
"""Create a principal subscription and return it."""
|
||||
start_date, end_date = self._calculate_dates(
|
||||
current_period_start, current_period_end, subscription.plan.days
|
||||
current_period_start, current_period_end, subscription.calulate_days()
|
||||
)
|
||||
|
||||
principal_subscription = PrincipalSubscription.objects.create(
|
||||
principal=principal,
|
||||
subscription=subscription,
|
||||
stripe_subscription_id=stripe_subscription or "Non Recurring",
|
||||
stripe_subscription_id=stripe_subscription,
|
||||
is_paid=True,
|
||||
auto_renew=bool(stripe_subscription),
|
||||
is_stripe_subscription=bool(stripe_subscription),
|
||||
order_id=order_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
grace_period_end_date=end_date + timedelta(days=15),
|
||||
grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(end_date),
|
||||
coupon_code=coupon.coupon_code if coupon else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -26,15 +26,14 @@ class SubscriptionForm(forms.ModelForm):
|
||||
model = Subscription
|
||||
fields = [
|
||||
"title",
|
||||
"stripe_product",
|
||||
"plan",
|
||||
"short_description",
|
||||
"interval",
|
||||
"interval_count",
|
||||
"high_amount",
|
||||
"amount",
|
||||
"short_description",
|
||||
"principal_types",
|
||||
"referral_percentage",
|
||||
"active",
|
||||
"deleted",
|
||||
"is_free",
|
||||
]
|
||||
|
||||
@@ -46,19 +45,6 @@ class SubscriptionForm(forms.ModelForm):
|
||||
id__in=[event_user.id, event_manager.id]
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
stripe_product = cleaned_data.get("stripe_product")
|
||||
|
||||
if not stripe_product:
|
||||
self.add_error(
|
||||
"stripe_product",
|
||||
"Please select a Stripe product to create a subscription.",
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class PrincipalSubscriptionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-08-18 18:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_subscriptions', '0010_principalsubscription_comments_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='product_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.2 on 2024-08-19 09:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_subscriptions', '0011_subscription_product_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='interval',
|
||||
field=models.CharField(choices=[('month', 'month'), ('day', 'day'), ('week', 'week'), ('year', 'year')], default='month', max_length=10),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='interval_count',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
]
|
||||
@@ -35,8 +35,20 @@ class StripeProduct(BaseModel):
|
||||
|
||||
|
||||
class Subscription(BaseModel):
|
||||
MONTH = "month"
|
||||
DAY = "day"
|
||||
WEEK = "week"
|
||||
YEAR = "year"
|
||||
|
||||
INTERVAL_TYPES = [
|
||||
(MONTH, "month"),
|
||||
(DAY, "day"),
|
||||
(WEEK, "week"),
|
||||
(YEAR, "year"),
|
||||
]
|
||||
title = models.CharField(max_length=255)
|
||||
price_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
product_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
stripe_product = models.ForeignKey(
|
||||
StripeProduct,
|
||||
related_name="subscription_product",
|
||||
@@ -50,6 +62,8 @@ class Subscription(BaseModel):
|
||||
plan = models.ForeignKey(
|
||||
Plan, related_name="subscription_plan", on_delete=models.CASCADE
|
||||
)
|
||||
interval = models.CharField(max_length=10, choices=INTERVAL_TYPES)
|
||||
interval_count = models.IntegerField(default=1)
|
||||
high_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00)
|
||||
amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00)
|
||||
principal_types = models.ManyToManyField(
|
||||
@@ -69,6 +83,7 @@ class Subscription(BaseModel):
|
||||
|
||||
def clean(self):
|
||||
# Ensure amount is greater than 1
|
||||
if not self.delete:
|
||||
if self.amount <= 1:
|
||||
raise ValidationError({"amount": "Amount must be greater than 1."})
|
||||
|
||||
@@ -78,16 +93,57 @@ class Subscription(BaseModel):
|
||||
{"high_amount": "High amount must be greater than amount."}
|
||||
)
|
||||
|
||||
# Ensure stripe_product is compulsory present
|
||||
# if not self.stripe_product:
|
||||
# raise ValidationError(
|
||||
# {"stripe_product": "Please select stripe product to create subscription."}
|
||||
# )
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean() # Call clean before saving to ensure validation
|
||||
from goodtimes.services import StripeService
|
||||
self.clean()
|
||||
if not self.is_free:
|
||||
if self.price_id:
|
||||
# Stipe dont provide to update the price record except active and deactive
|
||||
price = StripeService.retrieve_price(self.price_id)
|
||||
if not price["success"]:
|
||||
raise Exception(price['message'])
|
||||
|
||||
if self.active != price["data"].active:
|
||||
StripeService.update_price(price_id=self.price_id, active=self.active)
|
||||
else:
|
||||
# Create new product and price
|
||||
price = StripeService.create_price(
|
||||
product_data={
|
||||
"name": self.title,
|
||||
"description": self.short_description,
|
||||
},
|
||||
unit_amount=int(self.amount * 100),
|
||||
currency="gbp",
|
||||
recurring={
|
||||
"interval": self.plan.title,
|
||||
"interval_count": self.interval_count,
|
||||
},
|
||||
metadata={
|
||||
"subscription_id": self.id
|
||||
}
|
||||
|
||||
)
|
||||
if not price["success"]:
|
||||
raise Exception(price['message'])
|
||||
|
||||
# add the id in record
|
||||
self.price_id = price["data"].id
|
||||
self.product_id = price["data"].product
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def calculate_date(self):
|
||||
count = {
|
||||
self.DAY: 1,
|
||||
self.MONTH: 30, # assuming a month is 30 days
|
||||
self.YEAR: 365,
|
||||
self.WEEK: 7
|
||||
}
|
||||
|
||||
return count[self.interval] * self.interval_count
|
||||
|
||||
|
||||
|
||||
|
||||
class SubscriptionStatus(models.TextChoices):
|
||||
ACTIVE = "active", _("Active")
|
||||
@@ -139,7 +195,18 @@ class PrincipalSubscription(BaseModel):
|
||||
|
||||
@classmethod
|
||||
def has_principal_subscription(cls, principal):
|
||||
return cls.get_active_princial_subscription(principal).exists()
|
||||
return cls.get_grace_period_princial_subscription(principal).exists()
|
||||
|
||||
@classmethod
|
||||
def get_grace_period_princial_subscription(cls, principal):
|
||||
return cls.objects.filter(
|
||||
principal=principal,
|
||||
is_paid=True,
|
||||
# cancelled=False,
|
||||
active=True,
|
||||
# status=SubscriptionStatus.ACTIVE,
|
||||
grace_period_end_date__gt=timezone.now().date(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_active_princial_subscription(cls, principal):
|
||||
@@ -147,24 +214,28 @@ class PrincipalSubscription(BaseModel):
|
||||
principal=principal,
|
||||
is_paid=True,
|
||||
# cancelled=False,
|
||||
deleted=False,
|
||||
active=True,
|
||||
# status=SubscriptionStatus.ACTIVE,
|
||||
grace_period_end_date__gt=timezone.now().date(),
|
||||
end_date__gt=timezone.now().date(),
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_principal_subscription(cls, principal):
|
||||
return cls.objects.filter(
|
||||
principal=principal,
|
||||
is_paid=True,
|
||||
# cancelled=False,
|
||||
deleted=False,
|
||||
active=True,
|
||||
# status=SubscriptionStatus.ACTIVE,
|
||||
).order_by("-grace_period_end_date").first()
|
||||
|
||||
@classmethod
|
||||
def cancel_stipe_auto_renew_subscription(cls, subscription):
|
||||
subscription.status = SubscriptionStatus.INACTIVE
|
||||
subscription.auto_renew = False
|
||||
subscription.cancelled_date_time = timezone.now()
|
||||
subscription.save()
|
||||
|
||||
|
||||
class WebhookEvent(BaseModel):
|
||||
event_id = models.CharField(max_length=255, unique=True, db_index=True)
|
||||
|
||||
@@ -13,11 +13,6 @@ urlpatterns = [
|
||||
name="subscription_add",
|
||||
),
|
||||
path("subscription/<int:pk>/", views.SubscriptionDetailView.as_view(), name="subscription_detail"),
|
||||
# path(
|
||||
# "subscription/edit/<int:pk>/",
|
||||
# views.SubscriptionCreateOrUpdateView.as_view(),
|
||||
# name="subscription_edit",
|
||||
# ),
|
||||
path(
|
||||
"subscription/delete/<int:pk>",
|
||||
views.SubscriptionDeleteView.as_view(),
|
||||
@@ -32,28 +27,9 @@ urlpatterns = [
|
||||
views.StripeProductCreateOrUpdateView.as_view(),
|
||||
name="stripe_product_add",
|
||||
),
|
||||
# path(
|
||||
# "product/delete/<int:pk>",
|
||||
# views.StripeProductDeleteView.as_view(),
|
||||
# name="stripe_product_delete",
|
||||
# ),
|
||||
# PLANS
|
||||
path("plan/list/", views.PlanView.as_view(), name="plan_list"),
|
||||
# path(
|
||||
# "plan/add/",
|
||||
# views.PlanCreateOrUpdateView.as_view(),
|
||||
# name="plan_add",
|
||||
# ),
|
||||
# path(
|
||||
# "plan/edit/<int:pk>/",
|
||||
# views.PlanCreateOrUpdateView.as_view(),
|
||||
# name="plan_edit",
|
||||
# ),
|
||||
# path(
|
||||
# "plan/delete/<int:pk>",
|
||||
# views.PlanDeleteView.as_view(),
|
||||
# name="plan_delete",
|
||||
# ),
|
||||
|
||||
# Principal Subscription
|
||||
path(
|
||||
"principal_subscription/list/",
|
||||
@@ -97,7 +73,8 @@ urlpatterns = [
|
||||
),
|
||||
path("stripe/", views.SubscriptionPageView.as_view(), name="stripe"),
|
||||
path("active/", views.ActiveSubscriptionView.as_view(), name="active"),
|
||||
path("cancel-subscription/", views.CancelSubscriptionView.as_view(), name="cancel_subscription"),
|
||||
path("cancel-subscription/<int:subscription_id>", views.CancelAutoSubscriptionView.as_view(), name="cancel_subscription"),
|
||||
path("404/", views.ErrorView.as_view(), name="error"),
|
||||
path("success/", views.SuccessView.as_view(), name="success"),
|
||||
path("cancel/", views.CancelView.as_view(), name="cancel"),
|
||||
path("subscription-cancel-success/", views.SubscriptionCancelSuccessView.as_view(), name="subscription_cancel_success"),
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.contrib.auth import login
|
||||
import jwt
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
from goodtimes.services import StripeService
|
||||
from manage_coupons.models import Coupon
|
||||
from manage_subscriptions.forms import (
|
||||
StripeProductForm,
|
||||
@@ -112,57 +113,10 @@ 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
|
||||
stripe_product_id = (
|
||||
form.instance.stripe_product.product_id
|
||||
if form.instance.stripe_product
|
||||
else None
|
||||
)
|
||||
# creating Stripe price only if the subscription is not free
|
||||
if not form.cleaned_data.get("is_free") and stripe_product_id:
|
||||
# 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
|
||||
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
@@ -213,21 +167,6 @@ class SubscriptionDeleteView(LoginRequiredMixin, generic.View):
|
||||
try:
|
||||
# Retrieve the subscription object
|
||||
subscription = self.model.objects.get(id=pk)
|
||||
|
||||
# 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()
|
||||
@@ -346,126 +285,6 @@ class StripeProductView(LoginRequiredMixin, generic.ListView):
|
||||
return context
|
||||
|
||||
|
||||
""" we are not using product delete functionality because there may be multiple stripe's prices
|
||||
attached to one product and in case of any error it will mismatch the stripe's price with
|
||||
our database Subscription objects"""
|
||||
# class StripeProductDeleteView(LoginRequiredMixin, generic.View):
|
||||
# page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
# resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
# action = resource_action.ACTION_DELETE
|
||||
# model = StripeProduct
|
||||
# success_url = reverse_lazy("manage_subscriptions:stripe_product_list")
|
||||
# success_message = constants.RECORD_DELETED
|
||||
# error_message = constants.RECORD_NOT_FOUND
|
||||
|
||||
# def get(self, request, pk):
|
||||
# try:
|
||||
# # Retrieve the subscription object
|
||||
# product = self.model.objects.get(id=pk)
|
||||
|
||||
# # Fetching the related subscriptions (prices)
|
||||
# related_subscriptions = Subscription.objects.filter(stripe_product=product)
|
||||
|
||||
# # 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
|
||||
|
||||
# # Deactivating related prices on Stripe first
|
||||
# for subscription in related_subscriptions:
|
||||
# price_id = subscription.price_id
|
||||
# if price_id:
|
||||
# try:
|
||||
# stripe.Price.modify(price_id, active=False)
|
||||
# except stripe.error.StripeError as e:
|
||||
# # Handle Stripe errors
|
||||
# messages.error(request, f"Stripe error: {str(e)}")
|
||||
# return redirect(self.success_url)
|
||||
|
||||
# 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):
|
||||
# # 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/plan_add.html"
|
||||
# model = Plan
|
||||
# form_class = PlanForm
|
||||
# success_url = reverse_lazy("manage_subscriptions:plan_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 PlanView(LoginRequiredMixin, generic.ListView):
|
||||
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
@@ -483,28 +302,6 @@ class PlanView(LoginRequiredMixin, generic.ListView):
|
||||
return context
|
||||
|
||||
|
||||
# class PlanDeleteView(LoginRequiredMixin, generic.View):
|
||||
# page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
# resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
# action = resource_action.ACTION_DELETE
|
||||
# model = Plan
|
||||
# success_url = reverse_lazy("manage_subscriptions:plan_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)
|
||||
|
||||
# return redirect(self.success_url)
|
||||
|
||||
|
||||
class PrincipalSubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
@@ -621,117 +418,135 @@ class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View):
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class SubscriptionPageView(TemplateView):
|
||||
class SubscriptionPageView(generic.View):
|
||||
template_name = "stripe_html/index.html"
|
||||
model = Subscription
|
||||
error_url = reverse_lazy("manage_subscriptions:error")
|
||||
|
||||
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(
|
||||
def get(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseRedirect(self.error_url)
|
||||
|
||||
print("request user is :", request.user)
|
||||
obj = self.model.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
|
||||
if not obj.exists():
|
||||
print(f"No pre-define subscription details found in {self.model} table for user_type {request.user.principal_type}")
|
||||
return HttpResponseRedirect(self.error_url)
|
||||
|
||||
context = {
|
||||
"subscriptions": obj,
|
||||
# "stripeCheckoutUrl": request.build_absolute_uri(reverse("manage_subscriptions:create_checkout_session")),
|
||||
# "couponValidityCheckUrl": request.build_absolute_uri(reverse("manage_subscriptions:validate_coupon")),
|
||||
"stripeCheckoutUrl": settings.STRIPE_CHECKOUT_URL,
|
||||
"couponValidityCheckUrl": settings.COUPON_VALIDITY_CHECK_URL,
|
||||
"stripe_public_key": settings.STRIPE_PUBLISH_KEY
|
||||
}
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
class ActiveSubscriptionView(TemplateView):
|
||||
class ActiveSubscriptionView(generic.View):
|
||||
template_name = "stripe_html/active_subscription.html"
|
||||
model = IAmPrincipal
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
token = request.GET.get("token") or request.session.get("jwt")
|
||||
token = request.GET.get("token")
|
||||
print("token: ", token)
|
||||
|
||||
if token:
|
||||
request.session["jwt"] = token
|
||||
print("request.session: ", request.session)
|
||||
try:
|
||||
# Decode and validate token
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
||||
print("payload: ", payload)
|
||||
user = get_user_model().objects.get(id=payload["user_id"])
|
||||
user = self.model.objects.get(id=payload["user_id"])
|
||||
# Manually specify the authentication backend
|
||||
user.backend = "django.contrib.auth.backends.ModelBackend"
|
||||
# Log the user in
|
||||
login(request, user)
|
||||
print("Logged in user: ", user)
|
||||
except (
|
||||
IAmPrincipal.DoesNotExist,
|
||||
jwt.ExpiredSignatureError,
|
||||
jwt.InvalidTokenError,
|
||||
):
|
||||
return HttpResponseBadRequest("Invalid token or user not found")
|
||||
return HttpResponseRedirect(reverse("manage_subscriptions:error"))
|
||||
|
||||
today = timezone.now().date()
|
||||
if request.user.is_authenticated:
|
||||
latest_subscription = PrincipalSubscription.objects.filter(
|
||||
principal=request.user,
|
||||
is_paid=True,
|
||||
deleted=False,
|
||||
end_date__gte=today,
|
||||
).order_by('-end_date').last()
|
||||
|
||||
if not latest_subscription:
|
||||
return HttpResponseRedirect(reverse("manage_subscriptions:stripe"))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
request = self.request
|
||||
today = timezone.now().date()
|
||||
if request.user.is_authenticated:
|
||||
latest_subscription = PrincipalSubscription.objects.filter(
|
||||
principal=request.user,
|
||||
is_paid=True,
|
||||
deleted=False,
|
||||
end_date__gte=today,
|
||||
).order_by('-end_date').last()
|
||||
context["active_subscription"] = latest_subscription
|
||||
return context
|
||||
return render(request, self.template_name, context={"subscription": latest_subscription})
|
||||
return HttpResponseRedirect(reverse("manage_subscriptions:error"))
|
||||
|
||||
class CancelAutoSubscriptionView(LoginRequiredMixin, generic.View):
|
||||
model = PrincipalSubscription
|
||||
error_url = reverse_lazy("manage_subscriptions:error")
|
||||
|
||||
class CancelSubscriptionView(LoginRequiredMixin, generic.View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
subscription_id = request.POST.get("subscription_id")
|
||||
def get(self, request, *args, **kwargs):
|
||||
subscription_id = self.kwargs.get("subscription_id")
|
||||
|
||||
try:
|
||||
subscription = PrincipalSubscription.objects.get(
|
||||
subscription = self.model.objects.get(
|
||||
id=subscription_id, principal=request.user
|
||||
)
|
||||
except PrincipalSubscription.DoesNotExist:
|
||||
except self.model.DoesNotExist:
|
||||
messages.error(request, "Subscription not found.")
|
||||
return redirect("manage_subscriptions:cancel")
|
||||
return redirect("manage_subscriptions:error")
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
if subscription.is_stripe_subscription:
|
||||
# Cancel Stripe subscription
|
||||
stripe.Subscription.modify(
|
||||
subscription.stripe_subscription_id, cancel_at_period_end=True
|
||||
)
|
||||
if subscription.stripe_subscription_id:
|
||||
data = StripeService.cancel_auto_renew_subscription(subscription.stripe_subscription_id)
|
||||
if not data["success"]:
|
||||
return redirect(self.error_url)
|
||||
|
||||
# Updating subscription status in the local database
|
||||
subscription.status = SubscriptionStatus.INACTIVE
|
||||
subscription.cancelled = True
|
||||
subscription.auto_renew = False
|
||||
subscription.cancelled_date_time = timezone.now()
|
||||
subscription.save()
|
||||
self.model.cancel_stipe_auto_renew_subscription(subscription)
|
||||
|
||||
messages.success(request, "Subscription cancelled successfully.")
|
||||
return redirect("manage_subscriptions:subscription_cancel_success")
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
messages.error(request, f"Stripe error: {str(e)}")
|
||||
return redirect("manage_subscriptions:subscription_cancel_fails")
|
||||
except Exception as e:
|
||||
print(f'an error occur {str(e)}')
|
||||
messages.error(request, f"An error occurred while cancelling the subscription {str(e)}")
|
||||
return redirect(self.error_url)
|
||||
|
||||
return redirect(reverse_lazy("manage_subscriptions:active"))
|
||||
|
||||
# def post(self, request, *args, **kwargs):
|
||||
# subscription_id = request.POST.get("subscription_id")
|
||||
|
||||
# try:
|
||||
# subscription = PrincipalSubscription.objects.get(
|
||||
# id=subscription_id, principal=request.user
|
||||
# )
|
||||
# except PrincipalSubscription.DoesNotExist:
|
||||
# messages.error(request, "Subscription not found.")
|
||||
# return redirect("manage_subscriptions:cancel")
|
||||
|
||||
# try:
|
||||
# with transaction.atomic():
|
||||
# if subscription.is_stripe_subscription:
|
||||
# # Cancel Stripe subscription
|
||||
# stripe.Subscription.modify(
|
||||
# subscription.stripe_subscription_id, cancel_at_period_end=True
|
||||
# )
|
||||
|
||||
# # Updating subscription status in the local database
|
||||
# subscription.status = SubscriptionStatus.INACTIVE
|
||||
# subscription.cancelled = True
|
||||
# subscription.auto_renew = False
|
||||
# subscription.cancelled_date_time = timezone.now()
|
||||
# subscription.save()
|
||||
|
||||
# messages.success(request, "Subscription cancelled successfully.")
|
||||
# return redirect("manage_subscriptions:subscription_cancel_success")
|
||||
# except stripe.error.InvalidRequestError as e:
|
||||
# messages.error(request, f"Stripe error: {str(e)}")
|
||||
# return redirect("manage_subscriptions:subscription_cancel_fails")
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@@ -826,10 +641,12 @@ def create_checkout_session(request):
|
||||
data = json.loads(request.body)
|
||||
subscription_id = data.get("subscriptionId")
|
||||
coupon_code = data.get("couponCode")
|
||||
transaction_amount = data.get("discountAmount")
|
||||
transaction_amount = data.get("finalAmount")
|
||||
is_recurring = data.get("isRecurring")
|
||||
principal_id = request.user.id
|
||||
|
||||
print(f"subscription data is {subscription_id}, {coupon_code}, { is_recurring}")
|
||||
|
||||
try:
|
||||
subscription = Subscription.objects.get(id=subscription_id)
|
||||
except Subscription.DoesNotExist:
|
||||
@@ -842,14 +659,10 @@ def create_checkout_session(request):
|
||||
"success_url": request.build_absolute_uri("/subscriptions/success/"),
|
||||
"cancel_url": request.build_absolute_uri("/subscriptions/cancel/"),
|
||||
"metadata": {
|
||||
"transaction_amount": str(transaction_amount),
|
||||
"transaction_amount": str(subscription.amount),
|
||||
"principal": str(request.user.id),
|
||||
"subscription_id": str(subscription.id),
|
||||
"product_id": str(
|
||||
subscription.stripe_product.product_id
|
||||
if subscription.stripe_product
|
||||
else None
|
||||
),
|
||||
"product_id": subscription.product_id,
|
||||
"couponCode": coupon_code if coupon_code else None,
|
||||
},
|
||||
}
|
||||
@@ -900,6 +713,8 @@ def create_checkout_session(request):
|
||||
return JsonResponse({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
class ErrorView(TemplateView):
|
||||
template_name = "stripe_html/webview_404.html"
|
||||
class SuccessView(TemplateView):
|
||||
template_name = "stripe_html/success.html"
|
||||
|
||||
|
||||
@@ -48,7 +48,10 @@
|
||||
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;"> Plan Days </th>
|
||||
style="width: 69.2656px;"> Interval </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Interval Count </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Amount </th>
|
||||
@@ -58,9 +61,6 @@
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Free for Admin </th>
|
||||
<th class="checkbox-column sorting_asc" tabindex="0"
|
||||
aria-controls="style-3" aria-sort="ascending"
|
||||
style="width: 69.2656px;"> Stripe Product </th>
|
||||
<th class="sorting" tabindex="7" aria-controls="style-3"
|
||||
style="width: 79.7969px;">Active</th>
|
||||
<th class="dt-no-sorting sorting" tabindex="8"
|
||||
@@ -73,7 +73,8 @@
|
||||
<tr role="row">
|
||||
<td class="checkbox-column text-center sorting_1"> {{data_obj.id}}</td>
|
||||
<td>{{data_obj.title}}</td>
|
||||
<td>{{data_obj.plan.days}}</td>
|
||||
<td>{{data_obj.interval | capfirst}}</td>
|
||||
<td>{{data_obj.interval_count }}</td>
|
||||
<td>{{data_obj.amount}}</td>
|
||||
<td>
|
||||
{% if data_obj.principal_types.all %}
|
||||
@@ -88,13 +89,6 @@
|
||||
<td class="text-center">
|
||||
<span class="shadow-none badge {% if data_obj.is_free %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.is_free}}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if data_obj.stripe_product %}
|
||||
<span class="shadow-none badge badge-primary">{{ data_obj.stripe_product.product_id }}</span>
|
||||
{% else %}
|
||||
<span class="shadow-none badge badge-danger">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="shadow-none badge {% if data_obj.active %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.active}}</span>
|
||||
</td>
|
||||
|
||||
@@ -16,14 +16,13 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 40px 15px;
|
||||
padding: 0 15px 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--light-black);
|
||||
border: 1px solid var(--main-yellow);
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -93,7 +92,7 @@
|
||||
border-bottom: 2px solid #d4af37 !important;
|
||||
}
|
||||
.btn-outline-gold {
|
||||
color: #d4af37;
|
||||
color: #000;
|
||||
border-color: #d4af37;
|
||||
}
|
||||
.btn-outline-gold:hover {
|
||||
@@ -107,43 +106,39 @@
|
||||
<body>
|
||||
|
||||
<header class="text-center py-3">
|
||||
<h1 class="text-gold">Your Active Subscription</h1>
|
||||
<h1 class="text-gold">Subscription Details</h1>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="card bg-dark text-light border-gold">
|
||||
<div class="card-header border-bottom-gold">
|
||||
<h2 class="card-title text-gold">{{ active_subscription.subscription.title }}</h2>
|
||||
<h2 class="card-title text-gold">{{ subscription.subscription.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="text-gold">Full Name:</h5>
|
||||
<p>{{ active_subscription.principal.first_name }} {{ active_subscription.principal.last_name }}</p>
|
||||
<p><strong>Status:</strong> {{ active_subscription.get_status_display }}</p>
|
||||
<p><strong>Start Date:</strong> {{ active_subscription.start_date }}</p>
|
||||
<p><strong>End Date:</strong> {{ active_subscription.end_date }}</p>
|
||||
<p><strong>Auto Renew:</strong> {{ active_subscription.auto_renew|yesno:"Yes,No" }}</p>
|
||||
<p>{{ subscription.principal.first_name }} {{ subscription.principal.last_name }}</p>
|
||||
<p><strong>Status:</strong> {{ subscription.get_status_display }}</p>
|
||||
<p><strong>Start Date:</strong> {{ subscription.start_date }}</p>
|
||||
<p><strong>End Date:</strong> {{ subscription.end_date }}</p>
|
||||
<p><strong>Auto Renew:</strong> {{ subscription.auto_renew|yesno:"Yes,No" }}</p>
|
||||
|
||||
{% if active_subscription.coupon_code %}
|
||||
<p><strong>Coupon Code:</strong> {{ active_subscription.coupon_code }}</p>
|
||||
{% if subscription.coupon_code %}
|
||||
<p><strong>Coupon Code:</strong> {{ subscription.coupon_code }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if active_subscription.cancelled %}
|
||||
{% if subscription.cancelled_date_time %}
|
||||
<div class="cancel-details mt-4">
|
||||
<h3 class="text-gold">Cancellation Details</h3>
|
||||
<p><strong>Cancelled:</strong> Yes</p>
|
||||
<p><strong>Cancellation Date:</strong> {{ active_subscription.cancelled_date_time }}</p>
|
||||
<p><strong>Grace Period Ends:</strong> {{ active_subscription.grace_period_end_date }}</p>
|
||||
<h4 class="text-gold">Auto renew cancellation details</h4>
|
||||
<p><strong>Cancellation Date:</strong> {{ subscription.cancelled_date_time }}</p>
|
||||
<p><strong>Grace Period End Date:</strong> {{ subscription.grace_period_end_date }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if active_subscription.auto_renew and not active_subscription.cancelled %}
|
||||
{% if subscription.auto_renew and not subscription.cancelled_date_time %}
|
||||
<div class="cancel-details mt-4">
|
||||
<h3 class="text-gold">Cancel Subscription</h3>
|
||||
<form method="POST" action="{% url 'manage_subscriptions:cancel_subscription' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="subscription_id" value="{{ active_subscription.id }}">
|
||||
<button type="submit" class="btn btn-outline-gold">Cancel Subscription</button>
|
||||
</form>
|
||||
<h3 class="text-gold">Cancel Auto-Renewing Subscription</h3>
|
||||
<p>Click the button below to cancel your auto-renewing subscription. This will prevent future payments from being processed.</p>
|
||||
<a class="btn btn-outline-gold" href="{% url 'manage_subscriptions:cancel_subscription' subscription_id=subscription.id %}">Cancel Auto-Renewal</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -120,34 +120,25 @@
|
||||
{% else %}
|
||||
<p class="gold-text">£ {{ subscription.amount }}</p>
|
||||
{% endif %}
|
||||
{% if subscription.plan.days %}
|
||||
<p class="gold-text">Days of Subscription: {{ subscription.plan.days }}</p>
|
||||
{% else %}
|
||||
<p class="gold-text">Days of Subscription: Not available</p>
|
||||
{% endif %}
|
||||
<p class="gold-text">Subscription Cycle: {{subscription.interval_count}} {{ subscription.interval | capfirst }}</p>
|
||||
|
||||
</div>
|
||||
<div class="Adventure-btn text-center">
|
||||
<input type="text" placeholder="Enter Coupon Code" class="form-control coupon-code-input" size="20">
|
||||
{% comment %} <input type="text" name="coupon_code_{{subscription.id}}" placeholder="Enter Coupon Code" class="form-control" size="20"> {% endcomment %}
|
||||
<!-- Checkbox to select recurring or one-time payment -->
|
||||
<div class="form-check" style="display: flex; align-items: center; justify-content: center; margin-top: 10px;">
|
||||
<input class="form-check-input recurring-checkbox" type="checkbox" id="recurringCheck" style="margin-right: -4px; margin-top: -5px;">
|
||||
<label class="form-check-label gold-text" for="recurringCheck" style="margin: 0;">
|
||||
Recurring Subscription
|
||||
<input class="form-check-input recurring-checkbox" type="checkbox" id="recurringCheck_{{subscription.id}}" style="margin-right: -4px; margin-top: -5px;">
|
||||
<label class="form-check-label gold-text" for="recurringCheck_{{subscription.id}}" style="margin: 0;">
|
||||
Do you want to keep it auto-renew(recurring)
|
||||
</label>
|
||||
</div>
|
||||
<!-- Add a data attribute to store subscription ID -->
|
||||
<button class="common-btn subscribe-btn" data-subscription-id="{{ subscription.id }}" data-price-id="{{ subscription.price_id }}">Join now</button>
|
||||
<button class="common-btn subscribe-btn" data-subscription-id="{{ subscription.id }}">Join now</button>
|
||||
<!-- Error message container -->
|
||||
<div class="alert alert-danger coupon-error-message mt-2" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% empty %}
|
||||
<p>No subscriptions available.</p>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
@@ -523,38 +514,57 @@
|
||||
<script src="{% static 'src/assets/js/payment/custom.js' %}"></script>
|
||||
|
||||
<script>
|
||||
console.log("Sanity check!");
|
||||
var stripeCheckoutUrl = "{{ stripeCheckoutUrl }}";
|
||||
var stripeFinalUrl = "{{ stripeFinalUrl }}";
|
||||
var couponValidityCheckUrl = "{{ couponValidityCheckUrl }}";
|
||||
console.log("stripeCheckoutUrl: ", stripeCheckoutUrl);
|
||||
console.log("stripeFinalUrl: ", stripeFinalUrl);
|
||||
console.log("couponValidityCheckUrl: ", couponValidityCheckUrl);
|
||||
// Geting Stripe publishable key
|
||||
fetch(stripeCheckoutUrl)
|
||||
|
||||
// Initializing Stripe.js -- getting stripe public key to generate stripe object for creating checkout session
|
||||
const stripe = Stripe('{{stripe_public_key}}');
|
||||
document.querySelectorAll(".subscribe-btn").forEach(function(button) {
|
||||
button.addEventListener("click", function() {
|
||||
const subscriptionId = this.getAttribute("data-subscription-id");
|
||||
var recurringCheckbox = document.querySelector(`#recurringCheck_${subscriptionId}`);
|
||||
var isRecurring = recurringCheckbox ? recurringCheckbox.checked : false;
|
||||
|
||||
/*var couponCodeInput = document.querySelector(`input[name="coupon_code_${subscriptionId}"]`);
|
||||
var couponCode = couponCodeInput ? couponCodeInput.value : '';*/
|
||||
const errorMessageContainer = button.nextElementSibling;
|
||||
console.log("subscriptionId: ", subscriptionId);
|
||||
console.log("recurring: ", isRecurring); // Checking if the checkbox is checked
|
||||
button.disabled = true;
|
||||
button.previousElementSibling.value = "";
|
||||
|
||||
fetch(stripeCheckoutUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscriptionId: subscriptionId,
|
||||
// couponCode: couponCode,
|
||||
isRecurring: isRecurring
|
||||
}),
|
||||
})
|
||||
.then((result) => {
|
||||
return result.json();
|
||||
})
|
||||
.then((data) => {
|
||||
// Initializing Stripe.js -- getting stripe public key to generate stripe object for creating checkout session
|
||||
const stripe = Stripe(data.publicKey);
|
||||
console.log("loaded stripe public key");
|
||||
document.querySelectorAll(".subscribe-btn").forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
const subscriptionId = button.getAttribute("data-subscription-id");
|
||||
const priceId = button.getAttribute("data-price-id");
|
||||
const recurringCheckbox = button.closest('.feat-card').querySelector(".recurring-checkbox");
|
||||
const couponCode = button.previousElementSibling.value;
|
||||
const errorMessageContainer = button.nextElementSibling;
|
||||
console.log("subscriptionId: ", subscriptionId);
|
||||
console.log("couponCode: ", couponCode);
|
||||
console.log("priceId: ", priceId);
|
||||
console.log("recurring: ", recurringCheckbox.checked); // Checking if the checkbox is checked
|
||||
button.disabled = true;
|
||||
button.previousElementSibling.value = "";
|
||||
console.log("data: ", data);
|
||||
console.log("data.sessionId: ", data.sessionId);
|
||||
// Redirects to Stripe Checkout
|
||||
return stripe.redirectToCheckout({
|
||||
sessionId: data.sessionId
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
button.disabled = false;
|
||||
});
|
||||
|
||||
// Handling any coupon validation errors here before creating stripe final checkout session
|
||||
fetch(couponValidityCheckUrl, {
|
||||
/*fetch(couponValidityCheckUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -562,7 +572,7 @@
|
||||
body: JSON.stringify({
|
||||
subscriptionId: subscriptionId,
|
||||
couponCode: couponCode,
|
||||
isRecurring: recurringCheckbox.checked
|
||||
isRecurring: isRecurring
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
@@ -587,9 +597,8 @@
|
||||
body: JSON.stringify({
|
||||
subscriptionId: subscriptionId,
|
||||
couponCode: couponCode,
|
||||
priceId: priceId,
|
||||
finalAmount: finalAmount,
|
||||
isRecurring: recurringCheckbox.checked
|
||||
isRecurring: isRecurring
|
||||
}),
|
||||
})
|
||||
.then((result) => {
|
||||
@@ -613,8 +622,7 @@
|
||||
errorMessageContainer.style.display = 'block';
|
||||
errorMessageContainer.innerText = error.message;
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
});*/
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
129
templates/stripe_html/webview_404.html
Normal file
129
templates/stripe_html/webview_404.html
Normal file
@@ -0,0 +1,129 @@
|
||||
<!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>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap');
|
||||
|
||||
body {
|
||||
background-color: var(--black);
|
||||
color: var(--white);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 40px 15px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--light-black);
|
||||
border: 1px solid var(--main-yellow);
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid var(--main-yellow);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--main-yellow);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
font-size: 1rem;
|
||||
color: var(--white-mix);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(90.02deg, #CDA34C 0.02%, #F1D6A0 52%, #D1A956 98.68%);
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--black);
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #dc3545;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.cancel-details {
|
||||
background-color: #111;
|
||||
color: #bbb;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
.text-light {
|
||||
color: #f8f9fa !important;
|
||||
}
|
||||
.text-gold {
|
||||
color: #d4af37 !important;
|
||||
}
|
||||
.border-gold {
|
||||
border: 2px solid #d4af37 !important;
|
||||
}
|
||||
.border-bottom-gold {
|
||||
border-bottom: 2px solid #d4af37 !important;
|
||||
}
|
||||
.btn-outline-gold {
|
||||
color: #d4af37;
|
||||
border-color: #d4af37;
|
||||
}
|
||||
.btn-outline-gold:hover {
|
||||
background-color: #d4af37;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% comment %} <header class="text-center py-3">
|
||||
<h1 class="text-gold">Your Active Subscription</h1>
|
||||
</header> {% endcomment %}
|
||||
|
||||
<div class="container">
|
||||
<div class="card bg-dark text-light border-gold">
|
||||
<div class="card-header border-bottom-gold">
|
||||
<h2 class="card-title text-gold">404</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="text-gold">An error occurred. Please try again later.</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user