Merge pull request #90 from WDI-Ideas/feature/module_9_coupons
Feature/module 9 coupons
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
|
||||
@@ -708,7 +516,7 @@ class GoogleMapsservice:
|
||||
dict: Distance matrix response from Google Maps API.
|
||||
"""
|
||||
return self.client.distance_matrix(origin, destination)
|
||||
|
||||
|
||||
def search_address(self, address):
|
||||
"""
|
||||
Search for a list of addresses matching the given address string.
|
||||
@@ -1089,4 +897,274 @@ class InstagramPoster:
|
||||
result = self.instagram_api.post_image_with_caption(image_path, caption)
|
||||
if not result:
|
||||
return {'success': False, 'message': 'Error posting photo in Instagram.'}
|
||||
return {'success': True, 'message': 'Photo posted successfully'}
|
||||
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 create_coupon(
|
||||
amount_off: int = None,
|
||||
percent_off: float = None,
|
||||
duration: str = "once",
|
||||
name: str = None,
|
||||
currency: str = None,
|
||||
redeem_by: datetime = None,
|
||||
max_redemptions: int = 0,
|
||||
metadata: dict = None
|
||||
) -> dict:
|
||||
"""
|
||||
Creates a Stripe Coupon with either a fixed amount off or a percentage off.
|
||||
|
||||
:param amount_off: The discount amount to be applied (in the smallest currency unit, e.g., cents). This cannot be used in conjunction with `percent_off`.
|
||||
:param percent_off: The discount percentage to be applied to the price. This cannot be used in conjunction with `amount_off`.
|
||||
:param duration: The duration for which the coupon is valid. Valid values are:
|
||||
- "once": The coupon will apply to the next invoice only.
|
||||
:param name: An optional name for the coupon.
|
||||
:param currency: The currency in which the `amount_off` is specified. Required if `amount_off` is used.
|
||||
:param redeem_by: A timestamp at which the coupon will no longer be redeemable.
|
||||
The coupon can still be applied to invoices created after the `redeem_by` date,
|
||||
if the subscription was active prior to the date.
|
||||
:param max_redemptions: The maximum number of times this coupon can be redeemed in total.
|
||||
Defaults to 0, meaning unlimited redemptions.
|
||||
:param metadata: A set of key-value pairs to store additional information about the coupon in Stripe.
|
||||
|
||||
:return: A dictionary containing:
|
||||
- 'success': Boolean indicating the success of the operation.
|
||||
- 'data': The created Stripe Coupon object if successful.
|
||||
- 'message': Error message if the operation failed.
|
||||
|
||||
:raises ValueError: If both `amount_off` and `percent_off` are provided, or if neither is provided.
|
||||
Also raised if `amount_off` is provided without a corresponding `currency`.
|
||||
:raises stripe.error.StripeError: If an error occurs while creating the coupon via the Stripe API.
|
||||
|
||||
See: https://docs.stripe.com/api/coupons/create?lang=python
|
||||
"""
|
||||
if amount_off and percent_off:
|
||||
raise ValueError("You can provide either `amount_off` or `percent_off`, but not both.")
|
||||
|
||||
if not amount_off and not percent_off:
|
||||
raise ValueError("You must provide either `amount_off` or `percent_off`.")
|
||||
|
||||
if amount_off and not currency:
|
||||
raise ValueError("Currency must be provided when `amount_off` is specified.")
|
||||
|
||||
coupon_data = {
|
||||
"duration": duration,
|
||||
"name": name,
|
||||
"redeem_by": redeem_by,
|
||||
"max_redemptions": max_redemptions,
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
if amount_off:
|
||||
coupon_data.update({
|
||||
"amount_off": amount_off,
|
||||
"currency": currency,
|
||||
})
|
||||
elif percent_off:
|
||||
coupon_data.update({
|
||||
"percent_off": percent_off,
|
||||
})
|
||||
|
||||
try:
|
||||
coupon = stripe.Coupon.create(**coupon_data)
|
||||
return {'success': True, 'data': coupon}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error creating coupon: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def retrieve_coupon(coupon_id: str):
|
||||
"""
|
||||
Retrieve a Stripe Coupon by its ID.
|
||||
|
||||
:param coupon_id: The ID of the coupon to retrieve.
|
||||
:return: The retrieved Stripe Coupon object.
|
||||
|
||||
See: https://docs.stripe.com/api/coupons/retrieve?lang=python
|
||||
"""
|
||||
try:
|
||||
coupon = stripe.Coupon.retrieve(coupon_id)
|
||||
return {'success': True, 'data': coupon}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error retrieving coupon: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def delete_coupon(coupon_id: str):
|
||||
"""
|
||||
Retrieve a Stripe Coupon by its ID.
|
||||
|
||||
:param coupon_id: The ID of the coupon to retrieve.
|
||||
:return: The retrieved Stripe Coupon object.
|
||||
"""
|
||||
try:
|
||||
coupon = stripe.Coupon.delete(coupon_id)
|
||||
return {'success': True, 'data': coupon}
|
||||
except stripe.error.StripeError as e:
|
||||
return {'success': False, 'message': f"Error deleting coupon: {e}"}
|
||||
|
||||
@staticmethod
|
||||
def cancel_auto_renew_subscription(subscription_id: str):
|
||||
"""
|
||||
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://deciding-firmly-fly.ngrok-free.app/subscriptions/create-checkout-session/"
|
||||
COUPON_VALIDITY_CHECK_URL = "https://deciding-firmly-fly.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()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
import datetime
|
||||
from manage_subscriptions.models import PrincipalSubscription
|
||||
from manage_subscriptions.models import PrincipalSubscription, SubscriptionStatus
|
||||
|
||||
|
||||
class SubscriptionService:
|
||||
@@ -30,20 +30,21 @@ 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.calculate_days()
|
||||
)
|
||||
|
||||
PrincipalSubscription.objects.filter(principal=principal, status=SubscriptionStatus.ACTIVE).update(status=SubscriptionStatus.EXPIRED, active=False)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.0.2 on 2024-08-21 10:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_coupons', '0002_coupon_coupon_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='coupon',
|
||||
name='discount_amount',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='Representing the amount to subtract from an invoice total (required if discount_percentage is not passed)', max_digits=10, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='coupon',
|
||||
name='discount_percentage',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if discount_amount is not passed).', max_digits=5, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='coupon',
|
||||
name='max_redeems',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='coupon',
|
||||
name='valid_to',
|
||||
field=models.DateTimeField(help_text='Datetime for the last redeemable date. After this, the coupon is invalid for new customers.'),
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from accounts.models import BaseModel, IAmPrincipalType
|
||||
@@ -12,18 +13,21 @@ class Coupon(BaseModel):
|
||||
description = models.TextField(null=True, blank=True)
|
||||
image = models.ImageField(upload_to="coupon_img", null=True, blank=True)
|
||||
discount_amount = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, null=True, blank=True
|
||||
max_digits=10, decimal_places=2, null=True, blank=True, help_text="Representing the amount to subtract from an invoice total (required if discount_percentage is not passed)"
|
||||
)
|
||||
discount_percentage = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, null=True, blank=True
|
||||
max_digits=5, decimal_places=2, null=True, blank=True, help_text="A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if discount_amount is not passed)."
|
||||
)
|
||||
valid_from = models.DateTimeField()
|
||||
valid_to = models.DateTimeField()
|
||||
max_redeems = models.IntegerField(default=0)
|
||||
valid_to = models.DateTimeField(help_text="Datetime for the last redeemable date. After this, the coupon is invalid for new customers.")
|
||||
max_redeems = models.IntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
db_table = "coupon"
|
||||
|
||||
def __str__(self):
|
||||
return self.coupon_code
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validate the Coupon instance. Ensure that the `max_redeems` is greater than 0,
|
||||
@@ -61,19 +65,40 @@ class Coupon(BaseModel):
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean() # Call clean before saving to ensure validation
|
||||
super().save(*args, **kwargs)
|
||||
from goodtimes.services import StripeService
|
||||
if not self.delete:
|
||||
self.clean() # Call clean before saving to ensure validation
|
||||
|
||||
def __str__(self):
|
||||
return self.coupon_code
|
||||
if not self.pk and not self.coupon_id:
|
||||
amount_off = int(self.discount_amount * Decimal(100)) if self.discount_amount else None
|
||||
percent_off = float(self.discount_percentage) if self.discount_percentage else None
|
||||
|
||||
result = StripeService.create_coupon(
|
||||
amount_off=amount_off,
|
||||
percent_off=percent_off,
|
||||
duration="once",
|
||||
name=self.title,
|
||||
redeem_by=int(self.valid_to.timestamp()),
|
||||
max_redemptions=self.max_redeems,
|
||||
currency='gbp',
|
||||
metadata={"local_id": self.id}
|
||||
)
|
||||
|
||||
if not result["success"]:
|
||||
raise ValueError(f"Failed to create Stripe coupon: {result['message']}")
|
||||
|
||||
self.coupon_code = result['data'].id
|
||||
self.coupon_id = result["data"].id
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# If max_redeems is 0, it means that we are allowing unlimited redeems
|
||||
|
||||
def is_valid(self):
|
||||
now = timezone.now()
|
||||
return (
|
||||
self.active
|
||||
and not self.deleted
|
||||
and self.valid_from <= now <= self.valid_to
|
||||
and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems)
|
||||
)
|
||||
# def is_valid(self):
|
||||
# now = timezone.now()
|
||||
# return (
|
||||
# self.active
|
||||
# and not self.deleted
|
||||
# and self.valid_from <= now <= self.valid_to
|
||||
# and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems)
|
||||
# )
|
||||
|
||||
@@ -91,17 +91,9 @@ class CouponCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
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)
|
||||
)
|
||||
form.save()
|
||||
messages.success(request, self.get_success_message)
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class CouponDeleteView(LoginRequiredMixin, generic.View):
|
||||
|
||||
@@ -110,7 +110,6 @@ class Command(BaseCommand):
|
||||
return IAmPrincipalNotificationSettings.objects.filter(
|
||||
principal__principal_subscription__end_date=target_date,
|
||||
principal__principal_subscription__status=SubscriptionStatus.ACTIVE,
|
||||
principal__principal_subscription__cancelled=False,
|
||||
principal__principal_subscription__deleted=False,
|
||||
notification_category=NotificationCategoryChoices.SUBSCRIPTION,
|
||||
# is_enabled=True,
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
from django.contrib import admin
|
||||
from .models import (
|
||||
Plan,
|
||||
PrincipalSubscription,
|
||||
StripeProduct,
|
||||
Subscription,
|
||||
WebhookEvent,
|
||||
) # Update this with the correct import path for your models
|
||||
|
||||
|
||||
# Plan ModelAdmin
|
||||
class PlanAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "title", "days") # Include 'id' field here
|
||||
search_fields = ("title",) # Add search functionality by title
|
||||
|
||||
|
||||
# Register Plan with the admin site
|
||||
admin.site.register(Plan, PlanAdmin)
|
||||
|
||||
|
||||
# Subscription ModelAdmin
|
||||
class SubscriptionAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "title", "plan", "amount") # Include 'id' field here
|
||||
list_select_related = ("plan",) # Optimizes queries for the plan field
|
||||
list_display = ("id", "title", "interval", "amount") # Include 'id' field here
|
||||
list_select_related = ("interval",) # Optimizes queries for the interval field
|
||||
search_fields = (
|
||||
"title",
|
||||
"plan__title",
|
||||
) # Add search functionality by title and plan's title
|
||||
raw_id_fields = ("plan",) # Use a raw ID widget for the plan ForeignKey field
|
||||
"interval",
|
||||
) # Add search functionality by title and interval's title
|
||||
|
||||
|
||||
# Register Subscription with the admin site
|
||||
@@ -48,7 +35,7 @@ class PrincipalSubscriptionAdmin(admin.ModelAdmin):
|
||||
"is_paid",
|
||||
"auto_renew",
|
||||
"status",
|
||||
"cancelled",
|
||||
# "cancelled",
|
||||
) # Enable filtering by these fields
|
||||
search_fields = (
|
||||
"subscription__title",
|
||||
@@ -64,27 +51,6 @@ class PrincipalSubscriptionAdmin(admin.ModelAdmin):
|
||||
|
||||
admin.site.register(PrincipalSubscription, PrincipalSubscriptionAdmin)
|
||||
|
||||
|
||||
class StripeProductAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "title", "product_id", "default_price_id")
|
||||
search_fields = ("title", "product_id", "description")
|
||||
list_filter = ("default_price_id",)
|
||||
readonly_fields = ("product_id", "default_price_id")
|
||||
fields = (
|
||||
"title",
|
||||
"description",
|
||||
"metadata",
|
||||
"image_url",
|
||||
"product_id",
|
||||
"default_price_id",
|
||||
"active",
|
||||
"deleted",
|
||||
)
|
||||
|
||||
|
||||
admin.site.register(StripeProduct, StripeProductAdmin)
|
||||
|
||||
|
||||
@admin.register(WebhookEvent)
|
||||
class WebhookEventAdmin(admin.ModelAdmin):
|
||||
list_display = ("event_id", "received_at", "event_type", "processed_at", "status")
|
||||
|
||||
@@ -315,7 +315,7 @@ class CancelSubscription(APIView):
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
if subscription.is_stripe_subscription:
|
||||
if subscription.stripe_subscription_id:
|
||||
# Cancel Stripe subscription
|
||||
try:
|
||||
stripe.Subscription.modify(subscription.stripe_subscription_id, cancel_at_period_end=True)
|
||||
@@ -328,7 +328,7 @@ class CancelSubscription(APIView):
|
||||
|
||||
# Updating subscription status in the local database
|
||||
subscription.status = SubscriptionStatus.INACTIVE
|
||||
subscription.cancelled = True
|
||||
# subscription.cancelled = True
|
||||
subscription.cancelled_date_time = timezone.now()
|
||||
subscription.save()
|
||||
|
||||
|
||||
@@ -2,39 +2,22 @@ from django import forms
|
||||
from accounts.models import IAmPrincipalType
|
||||
from manage_subscriptions.models import (
|
||||
PrincipalSubscription,
|
||||
StripeProduct,
|
||||
Subscription,
|
||||
Plan,
|
||||
)
|
||||
|
||||
|
||||
class PlanForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Plan
|
||||
fields = ["title", "days"] # Include all fields you want from the model
|
||||
|
||||
# You can add custom validation for Plan fields here if needed
|
||||
# Example:
|
||||
# def clean_title(self):
|
||||
# title = self.cleaned_data.get('title')
|
||||
# # Add your validation logic here
|
||||
# return title
|
||||
|
||||
|
||||
class SubscriptionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
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,39 +29,24 @@ 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:
|
||||
model = PrincipalSubscription
|
||||
fields = "__all__" # Includes all fields from the model
|
||||
fields = [
|
||||
"subscription",
|
||||
"principal",
|
||||
"status",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"grace_period_end_date",
|
||||
"comments",
|
||||
"coupon_code"
|
||||
] # Includes all fields from the model
|
||||
widgets = {
|
||||
"start_date": forms.DateInput(attrs={"type": "date"}),
|
||||
"end_date": forms.DateInput(attrs={"type": "date"}),
|
||||
"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,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),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0.2 on 2024-08-21 18:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('manage_subscriptions', '0012_subscription_interval_subscription_interval_count'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='subscription',
|
||||
name='plan',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='stripeproduct',
|
||||
name='created_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='stripeproduct',
|
||||
name='modified_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='subscription',
|
||||
name='stripe_product',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='principalsubscription',
|
||||
name='cancelled',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='principalsubscription',
|
||||
name='is_stripe_subscription',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Plan',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='StripeProduct',
|
||||
),
|
||||
]
|
||||
@@ -5,51 +5,27 @@ from django.core.exceptions import ValidationError
|
||||
from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Plan(BaseModel):
|
||||
title = models.CharField(max_length=255)
|
||||
days = models.PositiveIntegerField()
|
||||
|
||||
class Meta:
|
||||
db_table = "plan"
|
||||
|
||||
def __str__(self):
|
||||
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):
|
||||
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)
|
||||
stripe_product = models.ForeignKey(
|
||||
StripeProduct,
|
||||
related_name="subscription_product",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
product_id = models.CharField(max_length=255, blank=True, null=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)
|
||||
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(
|
||||
@@ -78,16 +54,58 @@ 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
|
||||
if not self.delete:
|
||||
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.txitle,
|
||||
"description": self.short_description,
|
||||
},
|
||||
unit_amount=int(self.amount * 100),
|
||||
currency="gbp",
|
||||
recurring={
|
||||
"interval": self.interval,
|
||||
"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_days(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")
|
||||
@@ -112,13 +130,11 @@ class PrincipalSubscription(BaseModel):
|
||||
start_date = models.DateField()
|
||||
end_date = models.DateField()
|
||||
order_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
cancelled = models.BooleanField(default=False)
|
||||
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
|
||||
@@ -139,32 +155,43 @@ 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,
|
||||
active=True,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
grace_period_end_date__gt=timezone.now().date(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_active_princial_subscription(cls, principal):
|
||||
return cls.objects.filter(
|
||||
principal=principal,
|
||||
is_paid=True,
|
||||
# cancelled=False,
|
||||
deleted=False,
|
||||
active=True,
|
||||
# status=SubscriptionStatus.ACTIVE,
|
||||
grace_period_end_date__gt=timezone.now().date(),
|
||||
)
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
end_date__gt=timezone.now().date(),
|
||||
).order_by('-end_date').last()
|
||||
|
||||
|
||||
@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,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
).order_by("-grace_period_end_date").first()
|
||||
|
||||
@classmethod
|
||||
def cancel_stipe_auto_renew_subscription(cls, subscription):
|
||||
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,58 +13,18 @@ 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(),
|
||||
name="subscription_delete",
|
||||
),
|
||||
# Stripe Products
|
||||
path(
|
||||
"product/list/", views.StripeProductView.as_view(), name="stripe_product_list"
|
||||
),
|
||||
path(
|
||||
"product/add/",
|
||||
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/",
|
||||
views.PrincipalSubscriptionView.as_view(),
|
||||
name="principal_subscriptions_list",
|
||||
),
|
||||
# path(
|
||||
# "principal_subscription/add/",
|
||||
# views.PrincipalSubscriptionCreateOrUpdateView.as_view(),
|
||||
# name="principal_subscription_add",
|
||||
# ),
|
||||
path(
|
||||
"principal_subscription/edit/<int:pk>/",
|
||||
views.PrincipalSubscriptionCreateOrUpdateView.as_view(),
|
||||
@@ -80,11 +40,6 @@ urlpatterns = [
|
||||
views.PrincipalSubscriptionDeleteView.as_view(),
|
||||
name="principal_subscription_delete",
|
||||
),
|
||||
path(
|
||||
"stripe-subscription/",
|
||||
views.stripe_config,
|
||||
name="stripe_subscription",
|
||||
),
|
||||
path(
|
||||
"create-checkout-session/",
|
||||
views.create_checkout_session,
|
||||
@@ -97,7 +52,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"),
|
||||
|
||||
@@ -8,26 +8,6 @@ API_KEY = settings.GOOGLE_MAPS_API_KEY
|
||||
gmaps = googlemaps.Client(key=API_KEY)
|
||||
|
||||
|
||||
def get_active_subscription_id_for_principal(principal):
|
||||
# Filter subscriptions for the principal that are active and not cancelled
|
||||
active_subscriptions = PrincipalSubscription.objects.filter(
|
||||
principal=principal,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
is_paid=True,
|
||||
cancelled=False,
|
||||
deleted=False,
|
||||
active=True,
|
||||
end_date__gte=now().date(), # Ensure the subscription hasn't expired
|
||||
).order_by(
|
||||
"-end_date"
|
||||
) # Order by end_date to get the most recent active subscription
|
||||
|
||||
if active_subscriptions.exists():
|
||||
# Return the ID of the most recent active subscription
|
||||
return active_subscriptions.first().id
|
||||
return None
|
||||
|
||||
|
||||
def get_location_info(latitude, longitude):
|
||||
reverse_geocode_result = gmaps.reverse_geocode((latitude, longitude))
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ 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,
|
||||
SubscriptionForm,
|
||||
PrincipalSubscriptionForm,
|
||||
)
|
||||
@@ -22,8 +22,6 @@ from manage_wallets.models import (
|
||||
TransactionType,
|
||||
)
|
||||
from .models import (
|
||||
Plan,
|
||||
StripeProduct,
|
||||
Subscription,
|
||||
PrincipalSubscription,
|
||||
SubscriptionStatus,
|
||||
@@ -112,57 +110,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 +164,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()
|
||||
@@ -240,271 +176,6 @@ class SubscriptionDeleteView(LoginRequiredMixin, generic.View):
|
||||
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:stripe_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)
|
||||
|
||||
success, message = self.handle_stripe_product(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_product(self, form):
|
||||
try:
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
|
||||
stripe_product = stripe.Product.create(
|
||||
name=form.cleaned_data.get("title"),
|
||||
description=form.cleaned_data.get("description"),
|
||||
)
|
||||
|
||||
# Save Stripe Product ID to the form instance
|
||||
form.instance.product_id = stripe_product.id
|
||||
|
||||
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 StripeProductView(LoginRequiredMixin, generic.ListView):
|
||||
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
|
||||
action = resource_action.ACTION_READ
|
||||
model = StripeProduct
|
||||
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
|
||||
|
||||
|
||||
""" 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
|
||||
action = resource_action.ACTION_READ
|
||||
model = Plan
|
||||
template_name = "manage_subscriptions/plan_list.html"
|
||||
context_object_name = "plan_obj"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(deleted=False)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
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,124 +292,133 @@ 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(
|
||||
principal_types=request.user.principal_type,
|
||||
active=True,
|
||||
deleted=False,
|
||||
is_free=False,
|
||||
)
|
||||
def get(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseRedirect(self.error_url)
|
||||
|
||||
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
|
||||
print("request user is :", request.user)
|
||||
obj = self.model.objects.filter(
|
||||
principal_types=request.user.principal_type,
|
||||
active=True,
|
||||
is_free=False,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
class ActiveSubscriptionView(TemplateView):
|
||||
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(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()
|
||||
latest_subscription = PrincipalSubscription.get_active_princial_subscription(request.user)
|
||||
|
||||
print(f"latest subscription reodr is {latest_subscription}")
|
||||
|
||||
if not latest_subscription:
|
||||
return HttpResponseRedirect(reverse("manage_subscriptions:stripe"))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
return render(request, self.template_name, context={"subscription": latest_subscription})
|
||||
return HttpResponseRedirect(reverse("manage_subscriptions:error"))
|
||||
|
||||
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
|
||||
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"))
|
||||
|
||||
@csrf_exempt
|
||||
def stripe_config(request):
|
||||
if request.method == "GET":
|
||||
stripe_config = {"publicKey": settings.STRIPE_PUBLISH_KEY}
|
||||
return JsonResponse(stripe_config, safe=False)
|
||||
# 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,30 +506,27 @@ 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:
|
||||
return JsonResponse({"error": "Subscription not found."}, status=404)
|
||||
|
||||
# Default transaction amount based on subscription amount
|
||||
print("Before Session Data")
|
||||
session_data = {
|
||||
"payment_method_types": ["card"],
|
||||
"success_url": request.build_absolute_uri("/subscriptions/success/"),
|
||||
"cancel_url": request.build_absolute_uri("/subscriptions/cancel/"),
|
||||
"metadata": {
|
||||
"transaction_amount": str(transaction_amount),
|
||||
"principal": str(request.user.id),
|
||||
"transaction_amount": str(subscription.amount),
|
||||
"principal": str(principal_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 +577,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"
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user|has_resource_permission:resource_context.RESOURCE_MANAGE_COUPONS %}
|
||||
{% comment %} {% if user|has_resource_permission:resource_context.RESOURCE_MANAGE_COUPONS %}
|
||||
<li class="menu {% if page_name == resource_context.RESOURCE_MANAGE_COUPONS %}active{% endif %}">
|
||||
<a href="{% url 'manage_coupons:coupon_list'%}" aria-expanded="false"
|
||||
class="dropdown-toggle">
|
||||
@@ -164,7 +164,7 @@
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %} {% endcomment %}
|
||||
{% if user|has_resource_permission:resource_context.RESOURCE_PRINCIPAL_SUBSCRIPTIONS %}
|
||||
<li class="menu {% if page_name == resource_context.RESOURCE_PRINCIPAL_SUBSCRIPTIONS %}active{% endif %}">
|
||||
<a href="{% url 'manage_subscriptions:principal_subscriptions_list'%}" aria-expanded="false"
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
{% 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>{{operation}} {{page_name}}</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>
|
||||
{% comment %} <div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="title" aria-describedby="title">
|
||||
<div id="emailHelp" class="form-text" style="color: grey;">We'll never share your email with anyone else.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<div id="description"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="product-images">Image</label>
|
||||
<div class="multiple-file-upload">
|
||||
<div class="filepond--root filepond file-upload-multiple filepond--hopper" id="images" data-style-button-remove-item-position="left" data-style-button-process-item-position="right" data-style-load-indicator-position="right" data-style-progress-indicator-position="right" data-style-button-remove-item-align="false" style="height: 57px;"><input class="filepond--browser" type="file" id="filepond--browser-feeq8o6dj" name="filepond" aria-controls="filepond--assistant-feeq8o6dj" aria-labelledby="filepond--drop-label-feeq8o6dj" multiple=""><a class="filepond--credits" aria-hidden="true" href="https://pqina.nl/" target="_blank" rel="noopener noreferrer" style="transform: translateY(49px);">Powered by PQINA</a><div class="filepond--drop-label" style="transform: translate3d(0px, 0px, 0px); opacity: 1;"><label for="filepond--browser-feeq8o6dj" id="filepond--drop-label-feeq8o6dj" aria-hidden="true">Drag & Drop your files or <span class="filepond--label-action" tabindex="0">Browse</span></label></div><div class="filepond--list-scroller" style="transform: translate3d(0px, 41px, 0px);"><ul class="filepond--list" role="list"></ul></div><div class="filepond--panel filepond--panel-root" data-scalable="true"><div class="filepond--panel-top filepond--panel-root"></div><div class="filepond--panel-center filepond--panel-root" style="transform: translate3d(0px, 8px, 0px) scale3d(1, 0.41, 1);"></div><div class="filepond--panel-bottom filepond--panel-root" style="transform: translate3d(0px, 49px, 0px);"></div></div><span class="filepond--assistant" id="filepond--assistant-feeq8o6dj" role="status" aria-live="polite" aria-relevant="additions"></span><div class="filepond--drip"></div><fieldset class="filepond--data"></fieldset></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="tags">Tags</label>
|
||||
<input id="tags" class="tags" value="">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button> {% endcomment %}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
|
||||
{% include "cdn_through_html/filepond_cdn_js.html" %}
|
||||
{% include "cdn_through_html/quill_cdn_js.html" %}
|
||||
{% include "cdn_through_html/tagify_cdn_js.html" %}
|
||||
|
||||
|
||||
<script>
|
||||
/**
|
||||
* ===================================
|
||||
* Blog Description Editor
|
||||
* ===================================
|
||||
*/
|
||||
var quill = new Quill('#description', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block']
|
||||
]
|
||||
},
|
||||
placeholder: 'Write description...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* ====================
|
||||
* File Pond
|
||||
* ====================
|
||||
*/
|
||||
|
||||
// We want to preview images, so we register
|
||||
// the Image Preview plugin, We also register
|
||||
// exif orientation (to correct mobile image
|
||||
// orientation) and size validation, to prevent
|
||||
// large files from being added
|
||||
FilePond.registerPlugin(
|
||||
FilePondPluginImagePreview,
|
||||
FilePondPluginImageExifOrientation,
|
||||
FilePondPluginFileValidateSize,
|
||||
// FilePondPluginImageEdit
|
||||
);
|
||||
|
||||
// Select the file input and use
|
||||
// create() to turn it into a pond
|
||||
var ecommerce = FilePond.create(document.querySelector('.file-upload-multiple'));
|
||||
|
||||
|
||||
/**
|
||||
* =====================
|
||||
* Blog Tags
|
||||
* =====================
|
||||
*/
|
||||
// The DOM element you wish to replace with Tagify
|
||||
var input = document.querySelector('#id_tags');
|
||||
|
||||
// initialize Tagify on the above input node reference
|
||||
new Tagify(input,{
|
||||
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(', ')
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,107 +0,0 @@
|
||||
{% 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 Plans</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:subscription_list' %}">Subscriptions</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;"> Days </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 plan_obj %}
|
||||
<tr role="row">
|
||||
<td class="checkbox-column text-center sorting_1"> {{data_obj.id}}</td>
|
||||
<td>{{data_obj.title}}</td>
|
||||
<td>{{data_obj.days}}</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>
|
||||
|
||||
</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 %}
|
||||
@@ -53,16 +53,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Cancellation Button -->
|
||||
{% if principal_subscription_obj.auto_renew and not principal_subscription_obj.cancelled %}
|
||||
{% if principal_subscription_obj.auto_renew and not principal_subscription_obj.cancelled_date_time %}
|
||||
<div class="col-md-12 mb-4">
|
||||
<div class="card shadow-sm bg-dark text-light border-gold">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="text-gold">Cancel Subscription</h5>
|
||||
<form method="POST" action="{% url 'manage_subscriptions:cancel_subscription' %}">
|
||||
<a class="btn btn-primary" href="{% url 'manage_subscriptions:cancel_subscription' subscription_id=principal_subscription_obj.id %}">Cancel Subscription</a>
|
||||
{% comment %} <form method="POST" action="{% url 'manage_subscriptions:cancel_subscription' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="subscription_id" value="{{ principal_subscription_obj.id }}">
|
||||
<button type="submit" class="btn btn-outline-gold">Cancel Subscription</button>
|
||||
</form>
|
||||
</form> {% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,104 +0,0 @@
|
||||
{% 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:stripe_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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for data_obj in product_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">
|
||||
<span class="shadow-none badge {% if data_obj.active %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.active}}</span>
|
||||
</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 %}
|
||||
@@ -15,17 +15,7 @@
|
||||
<h3>Manage Subscriptions</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:stripe_product_list' %}">Products</a>
|
||||
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:subscription_add' %}">Add Subscriptions</a>
|
||||
<a class="btn btn-primary mb-2" href="{% url 'manage_subscriptions:stripe_product_add' %}">Add Stripe Product</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>
|
||||
|
||||
@@ -48,7 +38,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 +51,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 +63,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 +79,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,44 +106,40 @@
|
||||
<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 %}
|
||||
<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>
|
||||
</div>
|
||||
{% if subscription.auto_renew and not subscription.cancelled_date_time %}
|
||||
<div class="cancel-details mt-4">
|
||||
<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>
|
||||
</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,82 @@
|
||||
<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)
|
||||
.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 = "";
|
||||
|
||||
// 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 = "";
|
||||
|
||||
// Handling any coupon validation errors here before creating stripe final checkout session
|
||||
fetch(couponValidityCheckUrl, {
|
||||
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) => {
|
||||
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, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscriptionId: subscriptionId,
|
||||
couponCode: couponCode,
|
||||
isRecurring: isRecurring
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.error);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Creating checkout session for the selected subscription
|
||||
console.log("Data:", data);
|
||||
console.log("data.coupon:", data.coupon);
|
||||
const finalAmount = data.finalAmount;
|
||||
console.log("data.finalAmount:", finalAmount);
|
||||
fetch(stripeFinalUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -562,61 +597,34 @@
|
||||
body: JSON.stringify({
|
||||
subscriptionId: subscriptionId,
|
||||
couponCode: couponCode,
|
||||
isRecurring: recurringCheckbox.checked
|
||||
finalAmount: finalAmount,
|
||||
isRecurring: isRecurring
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.error);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
.then((result) => {
|
||||
return result.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Creating checkout session for the selected subscription
|
||||
console.log("Data:", data);
|
||||
console.log("data.coupon:", data.coupon);
|
||||
const finalAmount = data.finalAmount;
|
||||
console.log("data.finalAmount:", finalAmount);
|
||||
fetch(stripeFinalUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscriptionId: subscriptionId,
|
||||
couponCode: couponCode,
|
||||
priceId: priceId,
|
||||
finalAmount: finalAmount,
|
||||
isRecurring: recurringCheckbox.checked
|
||||
}),
|
||||
})
|
||||
.then((result) => {
|
||||
return result.json();
|
||||
})
|
||||
.then((data) => {
|
||||
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;
|
||||
});
|
||||
.then((data) => {
|
||||
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);
|
||||
errorMessageContainer.style.display = 'block';
|
||||
errorMessageContainer.innerText = error.message;
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
errorMessageContainer.style.display = 'block';
|
||||
errorMessageContainer.innerText = error.message;
|
||||
button.disabled = false;
|
||||
});*/
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
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">Something went wrong. Please try again later.</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user