Merge pull request #90 from WDI-Ideas/feature/module_9_coupons

Feature/module 9 coupons
This commit is contained in:
BOBBY VISHWAKARMA
2024-08-23 12:30:40 +05:30
committed by GitHub
31 changed files with 906 additions and 1406 deletions

View File

@@ -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}'}

View File

@@ -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"

View File

@@ -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/"

View File

@@ -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/"

View File

@@ -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 = (

View File

@@ -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()

View File

@@ -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,
)

View File

@@ -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.'),
),
]

View File

@@ -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)
# )

View File

@@ -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):

View File

@@ -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,

View File

@@ -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")

View File

@@ -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()

View File

@@ -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}),
}

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

@@ -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)

View File

@@ -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"),

View File

@@ -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))

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 &amp; 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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>