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

Feature/module 9 coupons
This commit is contained in:
BOBBY VISHWAKARMA
2024-08-13 15:48:01 +05:30
committed by GitHub
63 changed files with 2791 additions and 1437 deletions

View File

@@ -137,8 +137,10 @@ class PasswordResetSerializer(BasePasswordSerializer, serializers.ModelSerialize
model = IAmPrincipal
fields = ["password", "confirm_password"]
from phonenumbers import parse, phonenumberutil, NumberParseException
class ProfileSerializer(serializers.ModelSerializer):
profile_photo = serializers.ImageField(required=False)
principal_type_name = serializers.SerializerMethodField(read_only=True)
@@ -256,6 +258,7 @@ class ProfileExtendedDataSerializer(serializers.ModelSerializer):
"has_active_subscription": False,
"in_grace_period": False,
"grace_period_end_date": None,
"subscription_id": None,
}
today = timezone.now().date()
@@ -265,6 +268,9 @@ class ProfileExtendedDataSerializer(serializers.ModelSerializer):
print(f"subscrition record {latest_subscription}")
if latest_subscription:
subscription_status["subscription_id"] = (
latest_subscription.stripe_subscription_id
)
# Check if we're within the grace period
if today <= latest_subscription.grace_period_end_date:
subscription_status["has_active_subscription"] = (
@@ -387,4 +393,4 @@ class AppVersionSerializer(serializers.ModelSerializer):
class IAmPrincipalExtendedDataSerializer(serializers.ModelSerializer):
class Meta:
model = IAmPrincipalExtendedData
fields = "__all__"
fields = "__all__"

View File

@@ -44,6 +44,7 @@ def compute_resource_action_constants(request):
'RESOURCE_MANAGE_NOTIFICATIONS': resource_action.RESOURCE_MANAGE_NOTIFICATIONS,
'RESOURCE_MANAGE_REFERRALS': resource_action.RESOURCE_MANAGE_REFERRALS,
'RESOURCE_MANAGE_FEEDBACK': resource_action.RESOURCE_MANAGE_FEEDBACK,
'RESOURCE_MANAGE_COUPONS': resource_action.RESOURCE_MANAGE_COUPONS,
'RESOURCE_IAM_PRINCIPAL': resource_action.RESOURCE_IAM_PRINCIPAL,
'RESOURCE_IAM_PRINCIPAL_GROUP': resource_action.RESOURCE_IAM_PRINCIPAL_GROUP,
'RESOURCE_IAM_GROUP': resource_action.RESOURCE_IAM_GROUP,

View File

@@ -27,7 +27,8 @@ from accounts.resource_action import (
RESOURCE_MANAGE_REFERRALS,
RESOURCE_MANAGE_FEEDBACK,
RESOURCE_MANAGE_WITHDRAWALS,
RESOURCE_MANAGE_BANK_ACCOUNTS
RESOURCE_MANAGE_BANK_ACCOUNTS,
RESOURCE_MANAGE_COUPONS
)
# this variable store the data of model principaltype, action, resource
fixture_data = [
@@ -334,4 +335,16 @@ fixture_data = [
"action": [1, 2, 3, 4],
},
},
{
"model": "accounts.iamappresource",
"pk": 18,
"fields": {
"name": RESOURCE_MANAGE_COUPONS,
"label": RESOURCE_MANAGE_COUPONS,
"slug": RESOURCE_MANAGE_COUPONS,
"created_on": "2023-09-28T16:17:42.815",
"modified_on": "2023-09-28T16:17:42.815",
"action": [1, 2, 3, 4],
},
},
]

View File

@@ -386,5 +386,22 @@
4
]
}
},
{
"model": "accounts.iamappresource",
"pk": 18,
"fields": {
"name": "manage_coupons",
"label": "manage_coupons",
"slug": "manage_coupons",
"created_on": "2023-09-28T16:17:42.815",
"modified_on": "2023-09-28T16:17:42.815",
"action": [
1,
2,
3,
4
]
}
}
]

View File

@@ -28,6 +28,7 @@ RESOURCE_MANAGE_REFERRALS = "manage_referrals"
RESOURCE_MANAGE_NOTIFICATIONS = "manage_notifications"
RESOURCE_MANAGE_WITHDRAWALS = "manage_withdrawals"
RESOURCE_MANAGE_BANK_ACCOUNTS = "manage_bank_accounts"
RESOURCE_MANAGE_COUPONS = "manage_coupons"
# These constants are used solely for managing the active and inactive state of pages

View File

@@ -64,6 +64,7 @@ LOCAL_APPS = [
"manage_referrals",
"manage_cms",
"manage_communications", # for contact us, and feedback
"manage_coupons",
"manage_notifications.apps.ManageNotificationsConfig",
"chat",
]
@@ -302,8 +303,11 @@ SIMPLE_JWT = {
"JTI_CLAIM": "jti",
}
STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY")
STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY")
STRIPE_SECRET_KEY = "sk_test_51OexsKCesU6kunsIsbSKSZc1BF4gjklniaue8lmpkGKqDzenQtMkR8tKAryxErJXqp0jPiu1Gg7papa4tqZfKL9G00qUM4toB2"
STRIPE_PUBLISH_KEY = "pk_test_51OexsKCesU6kunsINDvKUhbelxeUmDAVZGSOisZ6XXHCp3pKtl4vs0pR42w0OcjZhngmECsXQNbAKNPOhiFMTJ8o00sRZQG0lh"
# STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY")
# STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY")
ONE_SIGNAL_APP_ID = env.str("ONE_SIGNAL_APP_ID")

View File

@@ -26,17 +26,17 @@ CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = ("http://localhost:3000",)
if DEBUG:
MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
INSTALLED_APPS += [
"debug_toolbar",
"django_extensions",
]
INTERNAL_IPS = [
"127.0.0.1",
]
# if DEBUG:
# MIDDLEWARE += [
# "debug_toolbar.middleware.DebugToolbarMiddleware",
# ]
# INSTALLED_APPS += [
# "debug_toolbar",
# "django_extensions",
# ]
# INTERNAL_IPS = [
# "127.0.0.1",
# ]
BASE_DOMAIN = "http://192.168.29.219:8000"
@@ -44,17 +44,15 @@ BASE_DOMAIN = "http://192.168.29.219:8000"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# STATIC_ROOT = os.path.join(BASE_DIR, "static")
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/"
LOGO_PATH = "static"

View File

@@ -82,5 +82,6 @@ STRIPE_CHECKOUT_URL = (
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/"
LOGO_PATH = "/var/www/goodtimes_prod/goodtimes/static"

View File

@@ -82,5 +82,6 @@ STRIPE_CHECKOUT_URL = (
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/"
LOGO_PATH = "/var/www/goodtimes/static"

View File

@@ -5,7 +5,7 @@ import colorlog
# from logging.handlers import TimedRotatingFileHandler
DEBUG = False
DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "goodtimes.betadelivery.com", "154.41.254.33"]
@@ -60,7 +60,7 @@ ALLOWED_HOSTS = ["127.0.0.1", "goodtimes.betadelivery.com", "154.41.254.33"]
# },
# }
# BASE_DOMAIN = "https://goodtimes.betadelivery.com"
BASE_DOMAIN = "https://goodtimes.betadelivery.com"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
@@ -81,3 +81,11 @@ STRIPE_CHECKOUT_URL = (
STRIPE_FINAL_URL = (
"https://goodtimes.betadelivery.com/subscriptions/create-checkout-session/"
)
COUPON_VALIDITY_CHECK_URL = (
"https://goodtimes.betadelivery.com/subscriptions/coupon-validity-check/"
)
LOGO_PATH = "/var/www/goodtimes/static"
STRIPE_TEST_MODE_SECRET_KEY = env.str("STRIPE_TEST_MODE_SECRET_KEY")
STRIPE_TEST_MODE_PUBLISH_KEY = env.str("STRIPE_TEST_MODE_PUBLISH_KEY")

View File

@@ -53,6 +53,9 @@ urlpatterns = [
path("subscriptions/", include("manage_subscriptions.urls")),
path("api/subscriptions/", include("manage_subscriptions.api.urls")),
path("coupons/", include("manage_coupons.urls")),
# path("api/coupons/", include("manage_coupons.api.urls")),
path("notifications/", include("manage_notifications.urls")),
path("api/notifications/", include("manage_notifications.api.urls")),
@@ -60,8 +63,9 @@ urlpatterns = [
# path('api/', include("accounts.api.urls")),
]
if settings.DEBUG:
import debug_toolbar
# if settings.DEBUG:
# import debug_toolbar
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += [path("__debug__/", include(debug_toolbar.urls))]
# urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# urlpatterns += [path("__debug__/", include(debug_toolbar.urls))]

View File

@@ -1,306 +0,0 @@
from django.conf import settings
from django.db import transaction
from django.shortcuts import get_object_or_404
from datetime import timedelta
from django.utils import timezone
from onesignal_sdk.client import Client as OneSignalClient
from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType
from manage_notifications.models import (
IAmPrincipalNotificationSettings,
InAppNotification,
NotificationCategoryChoices,
)
from manage_referrals.models import (
GoodTimeCoins,
ReferralRecord,
ReferralRecordReward,
ReferralTracking,
)
from django.core.exceptions import ObjectDoesNotExist
from manage_subscriptions.models import PrincipalSubscription, Subscription
from manage_wallets.models import (
TransactionStatus,
TransactionType,
Wallet,
Transaction,
)
import logging
logger = logging.getLogger(__name__)
class NotificationService:
def __init__(self):
self.client = OneSignalClient(
app_id=settings.ONE_SIGNAL_APP_ID, rest_api_key=settings.ONE_SIGNAL_API_KEY
)
def send_notification(self, title, message, player_id):
if player_id is None:
print("Player ID is None, skipping notification")
return
notification_payload = {
"headings": {"en": title},
"contents": {"en": message},
"include_player_ids": [player_id],
}
response = self.client.send_notification(notification_payload)
return response
def save_notification(self, principal, title, message, notification_category):
InAppNotification.objects.create(
principal=principal,
title=title,
message=message,
notification_category=notification_category,
)
def payment_success_notification(
self, principal, subscription, principal_subscription, amount
):
print("payment_success_notification: ", principal.player_id)
title = "Payment Successful"
end_date = principal_subscription.end_date
message = f"Your payment for {subscription} of ${amount} was successfully processed. Your subscription is valid till {end_date}"
self.send_notification(title, message, principal.player_id)
self.save_notification(
principal, title, message, NotificationCategoryChoices.TRANSACTION
)
def referral_received_notification(self, principal, amount, email):
print("referral_received_notification: ", principal.player_id)
title = "Congratulations! You got a referral G-Token."
message = f"Your referral {email} has subscribed to GoodTimesApp. You have received {amount} (£)"
self.save_notification(
principal, title, message, NotificationCategoryChoices.REFERRAL
)
if not self.should_send_referral_notification(principal):
print("Referral notifications are disabled for this user")
return
self.send_notification(title, message, principal.player_id)
def payment_failed_notification(self, principal, subscription, amount):
print("payment_failed_notification: ", principal.player_id)
title = "Payment Failed!"
message = f"Your payment for {subscription} of ${amount} was failed."
self.send_notification(title, message, principal.player_id)
self.save_notification(
principal, title, message, NotificationCategoryChoices.TRANSACTION
)
def should_send_referral_notification(self, principal):
notification_settings = get_object_or_404(
IAmPrincipalNotificationSettings,
principal=principal,
notification_category=NotificationCategoryChoices.REFERRAL,
)
return notification_settings.is_enabled
class WebhookService:
def __init__(self, webhook_data):
self.webhook_data = webhook_data
self.event_type = webhook_data["type"]
self.charge_data = webhook_data["data"]["object"]
def get_event_type(self):
return self.event_type
def get_principal(self):
principal_id = self.charge_data["metadata"]["principal"]
try:
return IAmPrincipal.objects.get(id=int(principal_id))
except (ObjectDoesNotExist, ValueError):
logger.error(f"Invalid principal ID: {principal_id}")
raise ValueError(f"Invalid principal ID: {principal_id}")
def get_transaction(self):
transaction_id = self.charge_data["metadata"]["transaction_id"]
try:
return Transaction.objects.get(id=int(transaction_id))
except (ObjectDoesNotExist, ValueError):
logger.error(f"Invalid transaction ID: {transaction_id}")
raise ValueError(f"Invalid transaction ID: {transaction_id}")
def get_subscription(self):
subscription_id = self.charge_data["metadata"]["subscription_id"]
try:
return Subscription.objects.get(id=int(subscription_id))
except (ObjectDoesNotExist, ValueError):
logger.error(f"Invalid subscription ID: {subscription_id}")
raise ValueError(f"Invalid subscription ID: {subscription_id}")
def get_order_id(self):
return self.charge_data["metadata"]["order_id"]
class ReferralRewardService:
def __init__(self, principal, principal_subscription, subscription):
self.notification_service = NotificationService()
self.principal = principal
self.principal_subscription = principal_subscription
self.subscription = subscription
def _fetch_referral_record(self):
return ReferralRecord.objects.filter(
referred_principal=self.principal,
is_completed=True,
active=True, # Assuming 'active' is a field determining if the record is currently relevant
deleted=False, # Assuming logical deletion is handled by a 'deleted' field
).first()
def _check_active_subscription(self, referrer_principal):
today = timezone.now().date()
return (
PrincipalSubscription.objects.filter(
principal=referrer_principal,
is_paid=True,
end_date__gte=today,
cancelled=False,
deleted=False,
)
.order_by("-end_date")
.first()
)
def _credit_reward(self, referral_record, subscription):
amount = subscription.referral_percentage * subscription.amount / 100
ReferralRecordReward.objects.create(
referral_record=referral_record,
subscription=subscription,
coins=1, # This value could be dynamically calculated or configured elsewhere
value=amount,
)
self._credit_transaction(referral_record.referrer_principal, amount)
self.notification_service.referral_received_notification(
referral_record.referrer_principal, amount, self.principal.email
)
def _credit_transaction(self, referrer_principal, amount):
print("_credit_transaction: ", referrer_principal)
Transaction.objects.create(
principal=referrer_principal,
transaction_type=TransactionType.CREDIT,
payment_method="",
transaction_status=TransactionStatus.SUCCESS,
amount=amount,
coins=1,
comment="Referral reward",
# Populate other fields as necessary, such as `order_id`, `product_id`, or `reference_id` if applicable
)
def _update_reward_status(self, referral_record, 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
is_referrer_subscribed = bool(active_subscription)
ReferralTracking.objects.create(
referral_record=referral_record,
referrer_subscription_id=referrer_subscription_id,
referred_subscription_id=referred_subscription_id,
is_referrer_subscribed=is_referrer_subscribed,
)
def credit_referral_reward_if_applicable(self):
referral_record = self._fetch_referral_record()
if referral_record:
active_subscription = self._check_active_subscription(
referral_record.referrer_principal
)
if active_subscription:
print("active_subscription: ", active_subscription)
if self.subscription:
print("self.subscription: ", self.subscription)
self._credit_reward(referral_record, self.subscription)
self._update_reward_status(referral_record, active_subscription)
class SubscriptionService:
def __init__(self):
self.principal_subscription = None
def create_principal_subscription(self, principal, subscription, order_id):
subscription_days = subscription.plan.days
today = timezone.now().date()
last_date = today + timedelta(days=subscription_days)
principal_subscription = PrincipalSubscription.objects.create(
principal=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
def update_transaction_success(self, principal_transaction, principal_subscription):
principal_transaction.transaction_status = TransactionStatus.SUCCESS
principal_transaction.principal_subscription = principal_subscription
principal_transaction.save()
def update_transaction_failure(self, principal_transaction):
principal_transaction.transaction_status = TransactionStatus.FAIL
principal_transaction.save()
class PaymentProcessingService:
def __init__(self, webhook_data):
self.webhook_service = WebhookService(webhook_data)
self.notification_service = NotificationService()
# Retrieve objects
self.principal = self.webhook_service.get_principal()
self.transaction = self.webhook_service.get_transaction()
self.subscription = self.webhook_service.get_subscription()
self.order_id = self.webhook_service.get_order_id()
self.subscription_service = SubscriptionService()
self.principal_subscription = None
def process_event(self):
if self.webhook_service.get_event_type() == "checkout.session.completed":
self.handle_success()
else:
self.handle_failure()
def handle_success(self):
with transaction.atomic():
# Create or update the principal subscription
self.principal_subscription = (
self.subscription_service.create_principal_subscription(
self.principal, self.subscription, self.order_id
)
)
print("First Part Done....!!!!!")
# Update transaction status to success
self.subscription_service.update_transaction_success(
self.transaction, self.principal_subscription
)
print("Second Part Done....!!!!!")
# Now handle referral rewards, if applicable
referral_service = ReferralRewardService(
self.principal, self.principal_subscription, self.subscription
)
print("Above Third Part...!!!!!!!!!!!")
referral_service.credit_referral_reward_if_applicable()
print("Third Part Done....!!!!!")
self.notification_service.payment_success_notification(
self.principal,
self.subscription,
self.principal_subscription,
self.transaction.amount,
)
def handle_failure(self):
self.subscription_service.update_transaction_failure(self.transaction)
# self.notification_service.payment_failed_notification(
# self.principal, self.subscription, self.transaction.amount
# )

View File

View File

@@ -0,0 +1,80 @@
from onesignal_sdk.client import Client as OneSignalClient
import logging
from manage_notifications.models import (
IAmPrincipalNotificationSettings,
InAppNotification,
NotificationCategoryChoices,
)
from django.shortcuts import get_object_or_404
from django.conf import settings
logger = logging.getLogger(__name__)
class NotificationService:
def __init__(self):
self.client = OneSignalClient(
app_id=settings.ONE_SIGNAL_APP_ID, rest_api_key=settings.ONE_SIGNAL_API_KEY
)
def send_notification(self, title, message, player_id):
if player_id is None:
print("Player ID is None, skipping notification")
return
notification_payload = {
"headings": {"en": title},
"contents": {"en": message},
"include_player_ids": [player_id],
}
response = self.client.send_notification(notification_payload)
return response
def save_notification(self, principal, title, message, notification_category):
InAppNotification.objects.create(
principal=principal,
title=title,
message=message,
notification_category=notification_category,
)
def payment_success_notification(
self, principal, subscription, principal_subscription, amount
):
print("payment_success_notification: ", principal.player_id)
title = "Payment Successful"
end_date = principal_subscription.end_date
message = f"Your payment for {subscription} of ${amount} was successfully processed. Your subscription is valid till {end_date}"
self.send_notification(title, message, principal.player_id)
self.save_notification(
principal, title, message, NotificationCategoryChoices.TRANSACTION
)
def referral_received_notification(self, principal, amount, email):
print("referral_received_notification: ", principal.player_id)
title = "Congratulations! You got a referral G-Token."
message = f"Your referral {email} has subscribed to GoodTimesApp. You have received {amount} (£)"
self.save_notification(
principal, title, message, NotificationCategoryChoices.REFERRAL
)
if not self.should_send_referral_notification(principal):
print("Referral notifications are disabled for this user")
return
self.send_notification(title, message, principal.player_id)
def payment_failed_notification(self, principal, subscription, amount):
print("payment_failed_notification: ", principal.player_id)
title = "Payment Failed!"
message = f"Your payment for {subscription} of ${amount} was failed."
self.send_notification(title, message, principal.player_id)
self.save_notification(
principal, title, message, NotificationCategoryChoices.TRANSACTION
)
def should_send_referral_notification(self, principal):
notification_settings = get_object_or_404(
IAmPrincipalNotificationSettings,
principal=principal,
notification_category=NotificationCategoryChoices.REFERRAL,
)
return notification_settings.is_enabled

View File

@@ -0,0 +1,180 @@
from venv import logger
from django.db import transaction
from manage_wallets.models import (
PaymentMethod,
Transaction,
TransactionStatus,
TransactionType,
)
from .notification_service import NotificationService
from .referral_reward_service import ReferralRewardService
from .subscription_service import SubscriptionService
from .webhook_service import WebhookService
class PaymentProcessingService:
def __init__(
self,
webhook_data,
stripe_subscription,
current_period_start,
current_period_end,
):
self.webhook_service = WebhookService(webhook_data)
self._order_id = None
self.notification_service = NotificationService()
self.subscription_service = SubscriptionService()
self.stripe_subscription = stripe_subscription
self.current_period_start = current_period_start
self.current_period_end = current_period_end
@property
def charge_data(self):
"""Return charge data from the webhook service."""
return self.webhook_service.charge_data
@property
def principal(self):
"""Return the principal from the webhook service."""
return self.webhook_service.get_principal()
@property
def subscription(self):
"""Return the subscription from the webhook service."""
return self.webhook_service.get_subscription()
@property
def order_id(self):
"""Return the order ID from the created transaction."""
return self._order_id
@order_id.setter
def order_id(self, value):
"""Set the order ID."""
self._order_id = value
@property
def coupon(self):
"""Return the coupon from the webhook service."""
return self.webhook_service.get_coupon()
@property
def amount(self):
"""Return the final amount from the webhook service."""
return self.webhook_service.get_final_amount()
def create_transaction(self):
"""Create a transaction based on webhook data."""
transaction = Transaction.objects.create(
principal=self.principal,
principal_subscription=None,
transaction_type=TransactionType.PAYMENT,
payment_method=PaymentMethod.CARD,
transaction_status=TransactionStatus.INITIATE,
amount=self.amount,
# order_id=self.order_id,
comment="Principal Subscription Initiated",
)
# Save the transaction to auto-generate the order_id
transaction.save()
# Step 1: Update the order_id in PaymentProcessingService
self.order_id = transaction.order_id
return transaction
def process_event(self):
"""Process the webhook event."""
try:
with transaction.atomic():
event_type = self.webhook_service.event_type
if event_type == "invoice.payment_succeeded" and self.charge_data.get("billing_reason") == "subscription_create":
logger.info(f"Skipping event {event_type} with billing reason 'subscription_create'")
return
txn = self.create_transaction()
if event_type in ["checkout.session.completed", "invoice.payment_succeeded"]:
self.handle_success(txn)
elif event_type in ["checkout.session.expired", "invoice.payment_failed"]:
self.handle_failure(txn)
else:
logger.warning(f"Unknown event type {event_type}. Skipping.")
return
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
raise
def handle_success(self, transaction):
"""Handle a successful payment."""
try:
self.create_principal_subscription(transaction)
self.process_referral_rewards()
self.send_success_notification(transaction)
self.update_transaction_status(
transaction,
TransactionStatus.SUCCESS,
self.subscription_service.principal_subscription,
)
except Exception as e:
self.handle_failure(transaction, error_message=str(e))
logger.error(f"Transaction Error: {str(e)}")
raise e
def create_principal_subscription(self, transaction):
"""Create or update the principal subscription."""
self.subscription_service.principal_subscription = (
self.subscription_service.create_principal_subscription(
principal=self.principal,
subscription=self.subscription,
stripe_subscription=self.stripe_subscription,
order_id=transaction.order_id,
current_period_start=self.current_period_start,
current_period_end=self.current_period_end,
coupon=self.coupon,
)
)
print("Principal Subscription Created")
def process_referral_rewards(self):
"""Handle referral rewards."""
referral_service = ReferralRewardService(
self.principal,
self.subscription_service.principal_subscription,
self.subscription,
)
referral_service.credit_referral_reward_if_applicable()
print("Referral Rewards Processed")
def send_success_notification(self, transaction):
"""Send a payment success notification."""
self.notification_service.payment_success_notification(
self.principal,
self.subscription,
self.subscription_service.principal_subscription,
transaction.amount,
)
print("Payment Success Notification Sent")
def handle_failure(self, transaction, error_message=None):
"""Handle a failed payment."""
self.update_transaction_status(
transaction, TransactionStatus.FAIL, error_message=error_message
)
self.notification_service.payment_failed_notification(
self.principal, self.subscription, transaction.amount
)
print("Payment Failure Notification Sent")
def update_transaction_status(
self, transaction, status, principal_subscription=None, error_message=None
):
"""Update the transaction status and associate with a subscription if provided."""
transaction.transaction_status = status
if principal_subscription:
transaction.principal_subscription = principal_subscription
if error_message:
transaction.error_message = error_message
transaction.save()

View File

@@ -0,0 +1,111 @@
from .notification_service import NotificationService
from manage_referrals.models import (
ReferralRecord,
ReferralRecordReward,
ReferralTracking,
)
from manage_wallets.models import Transaction, TransactionType, TransactionStatus
from django.utils import timezone
from manage_subscriptions.models import PrincipalSubscription
class ReferralRewardService:
def __init__(self, principal, principal_subscription, subscription):
self._notification_service = NotificationService()
self._principal = principal
self._principal_subscription = principal_subscription
self._subscription = subscription
@property
def principal(self):
return self._principal
@property
def principal_subscription(self):
return self._principal_subscription
@property
def subscription(self):
return self._subscription
@staticmethod
def _fetch_referral_record(principal):
"""Fetch the referral record for the given principal."""
return ReferralRecord.objects.filter(
referred_principal=principal,
is_completed=True,
active=True,
deleted=False,
).first()
@staticmethod
def _check_active_subscription(referrer_principal):
"""Check if the referrer principal has an active subscription."""
today = timezone.now().date()
return (
PrincipalSubscription.objects.filter(
principal=referrer_principal,
is_paid=True,
end_date__gte=today,
cancelled=False,
deleted=False,
)
.order_by("-end_date")
.first()
)
def _credit_reward(self, referral_record, subscription):
amount = subscription.referral_percentage * subscription.amount / 100
ReferralRecordReward.objects.create(
referral_record=referral_record,
subscription=subscription,
coins=1, # This value could be dynamically calculated or configured elsewhere
value=amount,
)
self._credit_transaction(referral_record.referrer_principal, amount)
self._notification_service.referral_received_notification(
referral_record.referrer_principal, amount, self.principal.email
)
def _credit_transaction(self, referrer_principal, amount):
"""Create a transaction record for the referral reward."""
print("referrer_principal: ", referrer_principal)
Transaction.objects.create(
principal=referrer_principal,
transaction_type=TransactionType.CREDIT,
payment_method="",
transaction_status=TransactionStatus.SUCCESS,
amount=amount,
coins=1,
comment="Referral reward",
)
def _update_reward_status(self, referral_record, active_subscription):
"""Update the status of the referral reward."""
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
is_referrer_subscribed = bool(active_subscription)
ReferralTracking.objects.create(
referral_record=referral_record,
referrer_subscription_id=referrer_subscription_id,
referred_subscription_id=referred_subscription_id,
is_referrer_subscribed=is_referrer_subscribed,
)
def credit_referral_reward_if_applicable(self):
"""Credit referral reward if applicable based on the referral record."""
referral_record = self._fetch_referral_record(self.principal)
if referral_record:
active_subscription = self._check_active_subscription(
referral_record.referrer_principal
)
if active_subscription and self.subscription:
self._credit_reward(referral_record, self.subscription)
self._update_reward_status(referral_record, active_subscription)

View File

@@ -0,0 +1,77 @@
from datetime import timedelta
from django.utils import timezone
import datetime
from manage_subscriptions.models import PrincipalSubscription
class SubscriptionService:
def __init__(self):
self._principal_subscription = None
@property
def principal_subscription(self):
"""Get the current principal subscription."""
return self._principal_subscription
@principal_subscription.setter
def principal_subscription(self, value):
"""Set the current principal subscription."""
self._principal_subscription = value
def create_principal_subscription(
self,
principal,
subscription,
stripe_subscription,
order_id,
current_period_start,
current_period_end,
coupon=None,
):
"""Create a principal subscription and return it."""
start_date, end_date = self._calculate_dates(
current_period_start, current_period_end, subscription.plan.days
)
principal_subscription = PrincipalSubscription.objects.create(
principal=principal,
subscription=subscription,
stripe_subscription_id=stripe_subscription or "Non Recurring",
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),
coupon_code=coupon.coupon_code if coupon else None,
)
if coupon:
self._update_coupon(coupon)
self.principal_subscription = principal_subscription
return principal_subscription
def _calculate_dates(
self, current_period_start, current_period_end, subscription_days
):
"""Calculate subscription start and end dates."""
today = timezone.now().date()
start_date = (
datetime.datetime.fromtimestamp(current_period_start).date()
if current_period_start
else today
)
end_date = (
datetime.datetime.fromtimestamp(current_period_end).date()
if current_period_end
else (today + timedelta(days=subscription_days))
)
return start_date, end_date
def _update_coupon(self, coupon):
"""Update coupon usage count."""
coupon.no_of_redeems += 1
coupon.save()
print("Coupon Saved Successfully!!!")

View File

@@ -0,0 +1,90 @@
from decimal import Decimal
from django.conf import settings
import stripe
from django.core.exceptions import ObjectDoesNotExist
from accounts.models import IAmPrincipal
from manage_coupons.models import Coupon
from manage_subscriptions.models import Subscription
import logging
logger = logging.getLogger(__name__)
stripe.api_key = settings.STRIPE_SECRET_KEY
class WebhookService:
def __init__(self, webhook_data):
self._webhook_data = webhook_data
self._event_type = webhook_data["type"]
self._charge_data = webhook_data["data"]["object"]
self._metadata = self._fetch_metadata()
def _fetch_metadata(self):
"""Fetch metadata based on the event type."""
if self._event_type == "checkout.session.completed":
return self._charge_data.get("metadata", {})
elif self._event_type == "invoice.payment_succeeded":
subscription_id = self._charge_data.get("subscription")
if subscription_id:
subscription = stripe.Subscription.retrieve(subscription_id)
return subscription.get("metadata", {})
return {}
@property
def event_type(self):
return self._event_type
@property
def charge_data(self):
return self._charge_data
def _get_object_from_metadata(self, model, id_key):
"""Retrieve object from metadata."""
obj_id = self._metadata.get(id_key)
if obj_id:
try:
return model.objects.get(id=int(obj_id))
except (ObjectDoesNotExist, ValueError) as e:
logger.error(f"Invalid {model.__name__} ID: {obj_id}")
raise ValueError(f"Invalid {model.__name__} ID: {obj_id}") from e
return None
def get_event_type(self):
return self.event_type
def get_principal(self):
"""Retrieve principal from metadata."""
return self._get_object_from_metadata(IAmPrincipal, "principal")
def get_subscription(self):
"""Retrieve subscription from metadata."""
return self._get_object_from_metadata(Subscription, "subscription_id")
def get_coupon(self):
"""Retrieve coupon from metadata."""
coupon_code = self._metadata.get("couponCode")
print("get_coupon:coupon_code: ", coupon_code)
if coupon_code:
try:
return Coupon.objects.get(coupon_code=coupon_code)
except Coupon.DoesNotExist:
logger.error(f"Invalid coupon code: {coupon_code}")
raise ValueError(f"Invalid coupon code: {coupon_code}")
return None
def get_final_amount(self):
"""Retrieve Amount after coupon discount from either stripe event or metadata."""
if self.event_type == "checkout.session.completed":
return (
Decimal(self._charge_data.get("amount_total", 0)) / 100
)
elif self.event_type == "invoice.payment_succeeded":
return (
Decimal(self._charge_data.get("amount_paid", 0)) / 100
)
# Fallback: Try to get the amount from metadata
return (
Decimal(self._metadata.get("metadata", {}).get("finalAmount", 0)) / 100
)

View File

40
manage_coupons/admin.py Normal file
View File

@@ -0,0 +1,40 @@
from django.contrib import admin
from .models import Coupon
class CouponAdmin(admin.ModelAdmin):
list_display = (
"id",
"title",
"coupon_id",
"coupon_code",
"discount_amount",
"discount_percentage",
"valid_from",
"valid_to",
"max_redeems",
"no_of_redeems",
"is_active",
)
search_fields = ("title", "coupon_code")
list_filter = ("valid_from", "valid_to", "max_redeems")
readonly_fields = ("no_of_redeems",)
fieldsets = (
(
None,
{"fields": ("title", "coupon_code", "coupon_id", "description", "image")},
),
(
"Discount Information",
{"fields": ("discount_amount", "discount_percentage")},
),
("Validity", {"fields": ("valid_from", "valid_to")}),
("Redemption", {"fields": ("max_redeems", "no_of_redeems")}),
)
def is_active(self, obj):
return obj.is_valid()
admin.site.register(Coupon, CouponAdmin)

View File

View File

View File

6
manage_coupons/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ManageCouponsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "manage_coupons"

24
manage_coupons/forms.py Normal file
View File

@@ -0,0 +1,24 @@
from django import forms
from django.core.exceptions import ValidationError
from manage_coupons.models import Coupon
class CouponForm(forms.ModelForm):
class Meta:
model = Coupon
fields = [
"title",
"description",
# "image",
"discount_amount",
"discount_percentage",
"valid_from",
"valid_to",
"max_redeems",
]
widgets = {
"valid_from": forms.DateTimeInput(attrs={"type": "datetime-local"}),
"valid_to": forms.DateTimeInput(attrs={"type": "datetime-local"}),
# "discount_amount": forms.NumberInput(attrs={'step': '0.01'}),
# "discount_percentage": forms.NumberInput(attrs={'step': '0.01'}),
}

View File

@@ -0,0 +1,81 @@
# Generated by Django 5.0.2 on 2024-07-22 12:20
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Coupon",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("active", models.BooleanField(default=True)),
("deleted", models.BooleanField(default=False)),
("created_on", models.DateTimeField(auto_now_add=True)),
("modified_on", models.DateTimeField(auto_now=True)),
("title", models.CharField(max_length=255)),
("coupon_code", models.CharField(max_length=50, unique=True)),
("no_of_redeems", models.IntegerField(default=0)),
("description", models.TextField(blank=True, null=True)),
(
"image",
models.ImageField(blank=True, null=True, upload_to="coupon_img"),
),
(
"discount_amount",
models.DecimalField(
blank=True, decimal_places=2, max_digits=10, null=True
),
),
(
"discount_percentage",
models.DecimalField(
blank=True, decimal_places=2, max_digits=5, null=True
),
),
("valid_from", models.DateTimeField()),
("valid_to", models.DateTimeField()),
("max_redeems", models.IntegerField(default=0)),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_created",
to=settings.AUTH_USER_MODEL,
),
),
(
"modified_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_modified",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "coupon",
},
),
]

View File

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

View File

79
manage_coupons/models.py Normal file
View File

@@ -0,0 +1,79 @@
from django.db import models
from django.utils import timezone
from accounts.models import BaseModel, IAmPrincipalType
from django.core.exceptions import ValidationError
class Coupon(BaseModel):
title = models.CharField(max_length=255)
coupon_code = models.CharField(max_length=50, unique=True)
coupon_id = models.CharField(max_length=255, blank=True, null=True)
no_of_redeems = models.IntegerField(default=0)
description = models.TextField(null=True, blank=True)
image = models.ImageField(upload_to="coupon_img", null=True, blank=True)
discount_amount = models.DecimalField(
max_digits=10, decimal_places=2, null=True, blank=True
)
discount_percentage = models.DecimalField(
max_digits=5, decimal_places=2, null=True, blank=True
)
valid_from = models.DateTimeField()
valid_to = models.DateTimeField()
max_redeems = models.IntegerField(default=0)
class Meta:
db_table = "coupon"
def clean(self):
"""
Validate the Coupon instance. Ensure that the `max_redeems` is greater than 0,
that either `discount_amount` or `discount_percentage` is set, and that
`valid_from` is earlier than `valid_to`.
"""
if self.max_redeems < 1:
raise ValidationError({"max_redeems": "Redeems must be more than 1."})
# Ensure discount_amount is non-negative
if self.discount_amount is not None and self.discount_amount < 1:
raise ValidationError(
{"discount_amount": "Discount amount must be more than 1."}
)
# Ensure discount_percentage is non-negative
if self.discount_percentage is not None and self.discount_percentage < 1:
raise ValidationError(
{"discount_percentage": "Discount percentage must be more than 1."}
)
if self.discount_amount and self.discount_percentage:
raise ValidationError(
"You can only set either a discount amount or a discount percentage, not both."
)
if not self.discount_amount and not self.discount_percentage:
raise ValidationError(
"You must set either a discount amount or a discount percentage."
)
if self.valid_from and self.valid_to and self.valid_from >= self.valid_to:
raise ValidationError(
"The valid_from date must be earlier than the valid_to date."
)
def save(self, *args, **kwargs):
self.clean() # Call clean before saving to ensure validation
super().save(*args, **kwargs)
def __str__(self):
return self.coupon_code
# 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)
)

3
manage_coupons/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

23
manage_coupons/urls.py Normal file
View File

@@ -0,0 +1,23 @@
from django.urls import path
from . import views
app_name = "manage_coupons"
urlpatterns = [
path("coupon/list/", views.CouponView.as_view(), name="coupon_list"),
path(
"coupon/add/",
views.CouponCreateOrUpdateView.as_view(),
name="coupon_add",
),
# path(
# "coupon/edit/<int:pk>/",
# views.CouponCreateOrUpdateView.as_view(),
# name="coupon_edit",
# ),
path(
"coupon/delete/<int:pk>/",
views.CouponDeleteView.as_view(),
name="coupon_delete",
),
]

47
manage_coupons/utils.py Normal file
View File

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

137
manage_coupons/views.py Normal file
View File

@@ -0,0 +1,137 @@
from django.conf import settings
from django.shortcuts import get_object_or_404, render, redirect
from django.views import generic
from django.contrib.auth.mixins import LoginRequiredMixin
import stripe
from accounts import resource_action
from django.urls import reverse_lazy
from django.contrib import messages
from goodtimes import constants
from manage_coupons.forms import CouponForm
from manage_coupons.models import Coupon
from manage_coupons.utils import handle_stripe_coupon
# Create your views here.
class CouponView(LoginRequiredMixin, generic.ListView):
page_name = resource_action.RESOURCE_MANAGE_COUPONS
resource = resource_action.RESOURCE_MANAGE_COUPONS
action = resource_action.ACTION_READ
model = Coupon
template_name = "manage_coupons/coupon_list.html"
context_object_name = "coupon_obj"
def get_queryset(self):
queryset = super().get_queryset().filter(deleted=False, active=True)
return queryset.order_by("-created_on")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_name"] = self.page_name
return context
class CouponCreateOrUpdateView(LoginRequiredMixin, generic.View):
# Set the page_name and resource
page_name = resource_action.RESOURCE_MANAGE_COUPONS
resource = resource_action.RESOURCE_MANAGE_COUPONS
# Initialize the action as ACTION_CREATE (can change based on logic)
action = resource_action.ACTION_CREATE
template_name = "manage_coupons/coupon_add.html"
model = Coupon
form_class = CouponForm
success_url = reverse_lazy("manage_coupons:coupon_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, request.FILES, 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 = handle_stripe_coupon(
form.instance, settings.STRIPE_SECRET_KEY
)
if success:
messages.success(self.request, message)
return redirect(self.success_url)
else:
messages.error(self.request, message)
return render(
request, self.template_name, context=self.get_context_data(form=form)
)
class CouponDeleteView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_COUPONS
resource = resource_action.RESOURCE_MANAGE_COUPONS
action = resource_action.ACTION_DELETE
model = Coupon
success_url = reverse_lazy("manage_coupons:coupon_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)
if type_obj.coupon_id:
stripe.api_key = settings.STRIPE_SECRET_KEY
try:
stripe.Coupon.delete(type_obj.coupon_id)
except stripe.error.StripeError as e:
# Handle Stripe errors
error_message = f"Stripe error: {e.user_message or e}"
messages.error(request, error_message)
return redirect(self.success_url)
type_obj.deleted = True
type_obj.active = False
type_obj.save()
messages.success(request, self.success_message)
except self.model.DoesNotExist:
messages.warning(request, self.error_message)
return redirect(self.success_url)

View File

@@ -2,6 +2,7 @@ from django.contrib import admin
from .models import (
Plan,
PrincipalSubscription,
StripeProduct,
Subscription,
WebhookEvent,
) # Update this with the correct import path for your models
@@ -64,6 +65,26 @@ 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

@@ -1,8 +1,5 @@
from django.urls import path
from . import views
from rest_framework_simplejwt.views import (
TokenRefreshView,
)
urlpatterns = [

View File

@@ -1,5 +1,6 @@
from datetime import timedelta
import datetime
import json
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils import timezone
@@ -7,9 +8,7 @@ from rest_framework import status
from rest_framework.views import APIView
from django.conf import settings
import stripe
from accounts.models import IAmPrincipal
import json
from goodtimes import constants, services
from goodtimes import constants
from manage_subscriptions.models import (
Subscription,
PrincipalSubscription,
@@ -35,7 +34,11 @@ from .serializers import PrincipalSubscriptionSerializer
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.response import Response
from goodtimes.webhook import PaymentProcessingService
from goodtimes.webhook.payment_processing_service import PaymentProcessingService
import logging
logger = logging.getLogger(__name__)
class CreatePrincipalSubscriptionApi(APIView):
@@ -178,13 +181,29 @@ class StripeWebhookTest(APIView):
event = stripe.Event.construct_from(json.loads(payload), stripe.api_key)
event_id = event["id"]
event_type = event["type"]
principal_id = event["data"]["object"]["metadata"]["principal"]
stripe_subscription_id = event["data"]["object"].get("subscription")
stripe_subscription = (
stripe.Subscription.retrieve(stripe_subscription_id)
if stripe_subscription_id
else None
)
current_period_start = (
stripe_subscription["current_period_start"]
if stripe_subscription
else None
)
current_period_end = (
stripe_subscription["current_period_end"]
if stripe_subscription
else None
)
webhook_event, created = WebhookEvent.objects.get_or_create(
event_id=event_id,
defaults={
"event_type": event_type,
"event_payload": json.loads(payload),
"event_payload": event,
},
)
@@ -194,62 +213,55 @@ class StripeWebhookTest(APIView):
message="Event already processed",
)
# Check if there is an active principal subscription
# if self._has_active_principal_subscription(principal_id):
# return ApiResponse.success(
# status=status.HTTP_208_ALREADY_REPORTED,
# message="Active principal subscription already exists",
# )
payment_service = PaymentProcessingService(
webhook_data=event,
stripe_subscription=stripe_subscription_id,
current_period_start=current_period_start,
current_period_end=current_period_end,
)
# payment_service = services.PaymentProcessingService(webhook_data=event)
payment_service = PaymentProcessingService(webhook_data=event)
payment_service.process_event()
webhook_event = WebhookEvent.objects.get(event_id=event_id)
webhook_event.status = "processed"
webhook_event.processed_at = timezone.now() # Make sure to import timezone
webhook_event.processed_at = timezone.now()
webhook_event.save()
return ApiResponse.success(
status=status.HTTP_200_OK, message="Event processed successfully"
)
except ValueError as e:
# Invalid payload
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Invalid payload",
errors=str(e),
)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
logger.error(f"Invalid Stripe signature: {str(e)}")
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Invalid signature",
errors=str(e),
)
except Transaction.DoesNotExist:
# Handle case where the transaction does not exist
except ValueError as e:
logger.error(f"Invalid payload: {str(e)}")
return ApiResponse.error(
status=status.HTTP_404_NOT_FOUND, message="Transaction not found"
status=status.HTTP_400_BAD_REQUEST,
message="Invalid payload",
errors=str(e),
)
except Transaction.DoesNotExist as e:
logger.error(f"Transaction does not exist: {str(e)}")
return ApiResponse.error(
status=status.HTTP_404_NOT_FOUND,
message="Transaction not found",
errors=str(e),
)
except Exception as e:
webhook_event.status = "failed"
webhook_event.error_message = str(e)
webhook_event.save()
logger.error(f"Error processing webhook event: {str(e)}")
if "webhook_event" in locals():
webhook_event.status = "failed"
webhook_event.error_message = str(e)
webhook_event.save()
return ApiResponse.error(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
message="Error processing event",
errors=str(e),
)
def _has_active_principal_subscription(self, principal_id):
return PrincipalSubscription.objects.filter(
principal__id=principal_id,
active=True,
deleted=False,
is_paid=True,
end_date__gte=timezone.now().date(),
).exists()
class LastActiveSubscriptionView(APIView):
authentication_classes = [JWTAuthentication]
@@ -281,3 +293,47 @@ class LastActiveSubscriptionView(APIView):
message="No Active Subscription Found",
errors="No Active Subscription Found",
)
class CancelSubscription(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
data = json.loads(request.body)
subscription_id = data.get("subscription_id")
try:
subscription = PrincipalSubscription.objects.get(
id=subscription_id, principal=request.user
)
except PrincipalSubscription.DoesNotExist:
return ApiResponse(
message=constants.FAILURE,
errors="Subscription not found.",
status=status.HTTP_404_NOT_FOUND,
)
with transaction.atomic():
if subscription.is_stripe_subscription:
# Cancel Stripe subscription
try:
stripe.Subscription.modify(subscription.stripe_subscription_id, cancel_at_period_end=True)
except stripe.error.InvalidRequestError as e:
return ApiResponse(
message=constants.FAILURE,
errors=f"Stripe error: {str(e)}",
status=status.HTTP_400_BAD_REQUEST,
)
# Updating subscription status in the local database
subscription.status = SubscriptionStatus.INACTIVE
subscription.cancelled = True
subscription.cancelled_date_time = timezone.now()
subscription.save()
return ApiResponse(
message=constants.SUCCESS,
data="Subscription cancelled successfully.",
status=status.HTTP_200_OK,
)

View File

@@ -1,5 +1,11 @@
from django import forms
from manage_subscriptions.models import PrincipalSubscription, Subscription, Plan
from accounts.models import IAmPrincipalType
from manage_subscriptions.models import (
PrincipalSubscription,
StripeProduct,
Subscription,
Plan,
)
class PlanForm(forms.ModelForm):
@@ -20,18 +26,38 @@ class SubscriptionForm(forms.ModelForm):
model = Subscription
fields = [
"title",
"stripe_product",
"plan",
"high_amount",
"amount",
"short_description",
# "long_description",
# "image",
"principal_types",
"referral_percentage",
"active",
"deleted",
"is_free",
] # Include all fields you want from the model
]
def __init__(self, *args, **kwargs):
super(SubscriptionForm, self).__init__(*args, **kwargs)
event_user = IAmPrincipalType.objects.get(name="event_user")
event_manager = IAmPrincipalType.objects.get(name="event_manager")
self.fields["principal_types"].queryset = IAmPrincipalType.objects.filter(
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):
@@ -44,3 +70,15 @@ class PrincipalSubscriptionForm(forms.ModelForm):
"grace_period_end_date": forms.DateInput(attrs={"type": "date"}),
"cancelled_date_time": forms.DateTimeInput(attrs={"type": "datetime"}),
}
class StripeProductForm(forms.ModelForm):
class Meta:
model = StripeProduct
fields = [
"title",
"description",
]
widgets = {
"description": forms.Textarea(attrs={"rows": 3}),
}

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-07-22 12:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_subscriptions", "0008_subscription_is_free"),
]
operations = [
migrations.AddField(
model_name="principalsubscription",
name="coupon_code",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

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

View File

@@ -1,6 +1,7 @@
from datetime import timedelta
from django.utils import timezone
from django.db import models
from django.core.exceptions import ValidationError
from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType
from django.utils.translation import gettext_lazy as _
@@ -18,8 +19,31 @@ class Plan(BaseModel):
return self.title
class StripeProduct(BaseModel):
title = models.CharField(max_length=255)
product_id = models.CharField(max_length=255, blank=True, null=True)
description = models.TextField(blank=True, null=True)
metadata = models.JSONField(blank=True, null=True)
image_url = models.URLField(blank=True, null=True)
default_price_id = models.CharField(max_length=255, blank=True, null=True)
class Meta:
db_table = "stripe_product"
def __str__(self):
return self.title
class Subscription(BaseModel):
title = models.CharField(max_length=255)
price_id = models.CharField(max_length=255, blank=True, null=True)
stripe_product = models.ForeignKey(
StripeProduct,
related_name="subscription_product",
on_delete=models.CASCADE,
null=True,
blank=True,
)
short_description = models.CharField(max_length=255, null=True, blank=True)
long_description = models.TextField(null=True, blank=True)
image = models.ImageField(upload_to="subscription_img", null=True, blank=True)
@@ -32,7 +56,10 @@ class Subscription(BaseModel):
IAmPrincipalType, related_name="principal_type_subscriptions", blank=True
)
referral_percentage = models.DecimalField(max_digits=5, decimal_places=2)
is_free = models.BooleanField(default=False, help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.")
is_free = models.BooleanField(
default=False,
help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.",
)
class Meta:
db_table = "subscription"
@@ -40,6 +67,27 @@ class Subscription(BaseModel):
def __str__(self):
return self.title
def clean(self):
# Ensure amount is greater than 1
if self.amount <= 1:
raise ValidationError({"amount": "Amount must be greater than 1."})
# Ensure high_amount is greater than amount
if self.high_amount <= self.amount:
raise ValidationError(
{"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
super().save(*args, **kwargs)
class SubscriptionStatus(models.TextChoices):
ACTIVE = "active", _("Active")
@@ -68,10 +116,14 @@ class PrincipalSubscription(BaseModel):
cancelled_date_time = models.DateTimeField(null=True, blank=True)
grace_period_end_date = models.DateField(null=True, blank=True)
stripe_customer_id = models.CharField(max_length=255, null=True, blank=True)
stripe_subscription_id = models.CharField(max_length=255, null=True, blank=True)
comments = models.CharField(max_length=255, null=True, blank=True)
is_stripe_subscription = models.BooleanField(default=False)
payment_intent_id = models.CharField(max_length=255, null=True, blank=True)
payment_intent_client_secret = models.CharField(
max_length=255, null=True, blank=True
)
coupon_code = models.CharField(max_length=255, null=True, blank=True)
class Meta:
db_table = "principal_subscription"

View File

@@ -12,15 +12,30 @@ urlpatterns = [
views.SubscriptionCreateOrUpdateView.as_view(),
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/edit/<int:pk>/",
views.SubscriptionCreateOrUpdateView.as_view(),
name="subscription_edit",
"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(
# "subscription/delete/<int:pk>",
# views.SubscriptionDeleteView.as_view(),
# name="subscription_delete",
# "product/delete/<int:pk>",
# views.StripeProductDeleteView.as_view(),
# name="stripe_product_delete",
# ),
# PLANS
path("plan/list/", views.PlanView.as_view(), name="plan_list"),
@@ -55,6 +70,11 @@ urlpatterns = [
views.PrincipalSubscriptionCreateOrUpdateView.as_view(),
name="principal_subscription_edit",
),
path(
"principal_subscription/<int:pk>",
views.PrincipalSubscriptionDetailView.as_view(),
name="principal_subscription_detail",
),
path(
"principal_subscription/delete/<int:pk>",
views.PrincipalSubscriptionDeleteView.as_view(),
@@ -70,8 +90,17 @@ urlpatterns = [
views.create_checkout_session,
name="create_checkout_session",
),
path(
"coupon-validity-check/",
views.validate_coupon,
name="validate_coupon",
),
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("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"),
path("subscription-cancel-fails/", views.SubscriptionCancelFailsView.as_view(), name="subscription_cancel_fails"),
# path("join-now/", views.IndexView.as_view(), name="index"),
]

View File

@@ -1,18 +1,17 @@
from datetime import timedelta
from decimal import Decimal
import json
from django.http import HttpResponseBadRequest, JsonResponse, HttpResponseRedirect
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
import stripe
from accounts import resource_action
from accounts.models import IAmPrincipal
from goodtimes.utils import ApiResponse
from django.contrib.auth import authenticate, login
from django.contrib.auth import login
import jwt
from rest_framework_simplejwt.tokens import AccessToken
from django.utils import timezone
from django.contrib.auth import get_user_model
from manage_coupons.models import Coupon
from manage_subscriptions.forms import (
PlanForm,
StripeProductForm,
SubscriptionForm,
PrincipalSubscriptionForm,
)
@@ -22,22 +21,27 @@ from manage_wallets.models import (
TransactionStatus,
TransactionType,
)
from .models import Plan, Subscription, PrincipalSubscription
from .models import (
Plan,
StripeProduct,
Subscription,
PrincipalSubscription,
SubscriptionStatus,
)
from django.views import generic
from rest_framework import status
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.urls import reverse, reverse_lazy
from django.contrib import messages
from goodtimes import constants
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.conf import settings
from rest_framework.permissions import IsAuthenticated
from django.views.generic.base import TemplateView
from django.db.models import Q
from django.db import transaction
# Create your views here.
from django.db.models import Q
class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View):
# Set the page_name and resource
@@ -101,14 +105,63 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View):
# This code ensures that only one free plan can be created by checking for existing free plans before saving a new one.
if form.cleaned_data.get("is_free"):
if self.model.objects.filter(Q(is_free=True) & Q(active=True)).exists():
messages.error(self.request, "A free plan is already available. Please deactivate the existing one before creating a new one.")
messages.error(
self.request,
"A free plan is already available. Please deactivate the existing one before creating a new one.",
)
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
@@ -119,7 +172,12 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView):
context_object_name = "subscription_obj"
def get_queryset(self):
queryset = super().get_queryset().filter(deleted=False, active=True).prefetch_related("principal_types")
queryset = (
super()
.get_queryset()
.filter(deleted=False, active=True)
.prefetch_related("principal_types")
)
return queryset.order_by("-created_on")
def get_context_data(self, **kwargs):
@@ -128,24 +186,219 @@ class SubscriptionView(LoginRequiredMixin, generic.ListView):
return context
# class SubscriptionDeleteView(LoginRequiredMixin, generic.View):
class SubscriptionDetailView(LoginRequiredMixin, generic.DetailView):
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
action = resource_action.ACTION_READ
model = Subscription
template_name = "manage_subscriptions/subscription_details.html"
context_object_name = "subscription"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_name"] = self.page_name
return context
class SubscriptionDeleteView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
action = resource_action.ACTION_DELETE
model = Subscription
success_url = reverse_lazy("manage_subscriptions:subscription_list")
success_message = constants.RECORD_DELETED
error_message = constants.RECORD_NOT_FOUND
def get(self, request, pk):
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()
messages.success(request, self.success_message)
except self.model.DoesNotExist:
messages.error(request, self.error_message)
return redirect(self.success_url)
class StripeProductCreateOrUpdateView(LoginRequiredMixin, generic.View):
# Set the page_name and resource
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
# Initialize the action as ACTION_CREATE (can change based on logic)
action = resource_action.ACTION_CREATE # Default action
template_name = "manage_subscriptions/product_add.html"
model = StripeProduct
form_class = StripeProductForm
success_url = reverse_lazy("manage_subscriptions: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 = Subscription
# success_url = reverse_lazy("manage_subscriptions:subscription_list")
# 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:
# type_obj = self.model.objects.get(id=pk)
# type_obj.deleted = True
# type_obj.active = False
# type_obj.save()
# # 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.success(request, self.error_message)
# messages.error(request, self.error_message)
# return redirect(self.success_url)
@@ -332,6 +585,20 @@ class PrincipalSubscriptionView(LoginRequiredMixin, generic.ListView):
return context
class PrincipalSubscriptionDetailView(LoginRequiredMixin, generic.DetailView):
page_name = resource_action.RESOURCE_PRINCIPAL_SUBSCRIPTIONS
resource = resource_action.RESOURCE_PRINCIPAL_SUBSCRIPTIONS
action = resource_action.ACTION_READ
model = PrincipalSubscription
template_name = "manage_subscriptions/principal_subscription_details.html"
context_object_name = "principal_subscription_obj"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_name"] = self.page_name
return context
class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS
@@ -357,8 +624,33 @@ class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View):
class SubscriptionPageView(TemplateView):
template_name = "stripe_html/index.html"
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,
)
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
class ActiveSubscriptionView(TemplateView):
template_name = "stripe_html/active_subscription.html"
def get(self, request, *args, **kwargs):
# Example of extracting the token from a query parameter or cookie
token = request.GET.get("token") or request.session.get("jwt")
print("token: ", token)
if token:
@@ -368,46 +660,80 @@ class SubscriptionPageView(TemplateView):
# Decode and validate token
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
print("payload: ", payload)
try:
UserModel = get_user_model()
user = UserModel.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:
# Handle expired token
return HttpResponseBadRequest("No Principal Found")
except jwt.ExpiredSignatureError:
# Handle expired token
return HttpResponseBadRequest("Expired Signature Error")
except jwt.InvalidTokenError:
return HttpResponseBadRequest("Invalid Token Error")
user = get_user_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")
today = timezone.now().date()
if request.user.is_authenticated:
latest_subscription = PrincipalSubscription.objects.filter(
principal=request.user,
is_paid=True,
deleted=False,
end_date__gte=today,
).order_by('-end_date').last()
if not latest_subscription:
return HttpResponseRedirect(reverse("manage_subscriptions:stripe"))
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
request = self.request
today = timezone.now().date()
if request.user.is_authenticated:
print("request.user: ", request.user)
subscriptions = Subscription.objects.filter(
principal_types=request.user.principal_type, active=True, deleted=False, is_free=False
)
if subscriptions.exists():
context["subscriptions"] = subscriptions
context["stripeCheckoutUrl"] = settings.STRIPE_CHECKOUT_URL
context["stripeFinalUrl"] = settings.STRIPE_FINAL_URL
else:
# Handle the case where no subscriptions are found for the principal type.
context["error"] = "No subscriptions found for your user type."
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 CancelSubscriptionView(LoginRequiredMixin, generic.View):
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.active = 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
def stripe_config(request):
if request.method == "GET":
@@ -415,104 +741,163 @@ def stripe_config(request):
return JsonResponse(stripe_config, safe=False)
def has_active_principal_subscription(principal_id):
return PrincipalSubscription.objects.filter(
principal__id=principal_id,
active=True,
deleted=False,
cancelled=False,
is_paid=True,
end_date__gte=timezone.now().date(),
).exists()
@csrf_exempt
@require_POST
def create_checkout_session(request):
success_url = reverse_lazy("manage_subscriptions:stripe")
stripe.api_key = settings.STRIPE_SECRET_KEY
def validate_coupon(request):
data = json.loads(request.body)
print("data: ", data)
coupon_code = data.get("couponCode", None)
subscription_id = data.get("subscriptionId", None)
principal_id = request.user.id
# if has_active_principal_subscription(principal_id):
# print("Active principal subscription already exists.")
# return JsonResponse(
# {"error": "Active principal subscription already exists"}, status=400
# )
final_amount = None
try:
subscription = Subscription.objects.get(id=subscription_id)
except Subscription.DoesNotExist:
return JsonResponse({"error": "Subscription not found."}, status=404)
order_id = (
"order_" + str(timezone.localtime().timestamp()) + str(request.user.email)
)
print("order_id: ", order_id)
# If no coupon code is provided, assume no discount and proceed
if not coupon_code:
return JsonResponse({"message": "No coupon code provided."}, status=200)
# Create a Transaction object with status INITIATE
transaction = Transaction.objects.create(
principal=request.user,
principal_subscription=None, # Since the subscription is not created yet
transaction_type=TransactionType.PAYMENT, # or PAYMENT, as applicable
payment_method=PaymentMethod.CARD, # Assuming CARD for this example
transaction_status=TransactionStatus.INITIATE,
amount=subscription.amount, # Fetching amount from the Subscription object
order_id=order_id,
comment="Principal Subscription Initiated",
)
# Validating Coupon
try:
coupon = Coupon.objects.get(coupon_code=coupon_code)
if not coupon.is_valid():
return JsonResponse({"error": "Coupon is not valid."}, status=400)
# Check discount amount
if coupon.discount_amount and coupon.discount_amount > subscription.amount:
final_amount = subscription.amount - coupon.discount_amount
return JsonResponse(
{"error": "Coupon discount amount exceeds subscription amount."},
status=400,
)
# Check discount percentage
if coupon.discount_percentage:
discount = (
coupon.discount_percentage / Decimal("100")
) * subscription.amount
if discount > subscription.amount:
return JsonResponse(
{
"error": "Coupon discount percentage exceeds subscription amount."
},
status=400,
)
final_amount = subscription.amount - discount
# Retrieving coupon from Stripe if applicable
if coupon.coupon_id:
try:
stripe_coupon = stripe.Coupon.retrieve(coupon.coupon_id)
print("stripe_coupon: ", stripe_coupon)
if (
stripe_coupon.max_redemptions
and stripe_coupon.times_redeemed >= stripe_coupon.max_redemptions
):
return JsonResponse(
{"error": "Coupon max redeems reached."}, status=400
)
return JsonResponse(
{"data": {"coupon": stripe_coupon, "finalAmount": final_amount}},
status=200,
)
except stripe.error.InvalidRequestError:
return JsonResponse(
{"error": f"Invalid coupon code: {coupon_code}"}, status=400
)
except stripe.error.StripeError as e:
return JsonResponse({"error": f"Stripe error: {str(e)}"}, status=400)
else:
return JsonResponse(
{
"error": "Coupon is either invalid, expired, or not associated with Stripe."
},
status=400,
)
except Coupon.DoesNotExist:
return JsonResponse({"error": "Coupon not found."}, status=404)
@csrf_exempt
@require_POST
def create_checkout_session(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
data = json.loads(request.body)
subscription_id = data.get("subscriptionId")
coupon_code = data.get("couponCode")
transaction_amount = data.get("discountAmount")
is_recurring = data.get("isRecurring")
principal_id = request.user.id
try:
# customer = stripe.Customer.create(
# email=request.user.email,
# shipping={
# "name": request.user.first_name,
# "address": {
# "line1": request.user.city,
# "city": request.user.city,
# "postal_code": "SW1A 2AA",
# "country": request.user.address_line1, # Adjust accordingly
# },
# },
# )
subscription = Subscription.objects.get(id=subscription_id)
except Subscription.DoesNotExist:
return JsonResponse({"error": "Subscription not found."}, status=404)
# Create a checkout session
checkout_session = stripe.checkout.Session.create(
payment_method_types=["card"],
# customer=customer.id, # Optional: Link the session to the Stripe customer created above
line_items=[
# 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),
"subscription_id": str(subscription.id),
"product_id": str(
subscription.stripe_product.product_id
if subscription.stripe_product
else None
),
"couponCode": coupon_code if coupon_code else None,
},
}
print("session_data: ", session_data)
# Coupon Handling
if coupon_code:
try:
stripe_coupon = stripe.Coupon.retrieve(coupon_code)
session_data["discounts"] = [{"coupon": stripe_coupon.id}]
except stripe.error.InvalidRequestError:
return JsonResponse(
{"error": f"Invalid coupon code: {coupon_code}"}, status=400
)
except stripe.error.StripeError as e:
return JsonResponse({"error": f"Stripe error: {str(e)}"}, status=400)
# Creating the Stripe Checkout Session
try:
if is_recurring and subscription.price_id:
session_data["line_items"] = [
{
"price": subscription.price_id,
"quantity": 1,
}
]
session_data["mode"] = "subscription"
session_data["subscription_data"] = {
"metadata": session_data["metadata"],
}
else:
session_data["line_items"] = [
{
"price_data": {
"currency": "gbp",
"product_data": {
"name": subscription.title, # Adjust with your subscription/product name
"name": subscription.title,
"description": subscription.short_description,
},
"unit_amount": int(
subscription.amount * 100
), # Unit amount in cents/pence
"tax_behavior": "inclusive", # or 'exclusive', based on your tax settings
"unit_amount": int(subscription.amount * 100),
},
"quantity": 1,
}
],
# allow_promotion_codes=True,
mode="payment",
# discounts=[{"coupon": "VLMAsicx"}],
success_url=request.build_absolute_uri("/subscriptions/success/"),
cancel_url=request.build_absolute_uri("/subscriptions/cancel/"),
metadata={
"principal": str(request.user.id),
"order_id": str(order_id),
"subscription_id": str(subscription.id),
"transaction_id": str(transaction.id),
# "principal_subscription_id": str(principal_subscription.id),
},
)
]
session_data["mode"] = "payment"
checkout_session = stripe.checkout.Session.create(**session_data)
return JsonResponse({"sessionId": checkout_session["id"]})
except Exception as e:
return JsonResponse({"error": str(e)})
return JsonResponse({"error": str(e)}, status=500)
class SuccessView(TemplateView):
@@ -523,36 +908,9 @@ class CancelView(TemplateView):
template_name = "stripe_html/cancel.html"
# class IndexView(TemplateView):
# template_name = "stripe_html/index.html"
class SubscriptionCancelSuccessView(TemplateView):
template_name = "stripe_html/subscription_cancel_success.html"
# def get(self, request, *args, **kwargs):
# # Example of extracting the token from a query parameter or cookie
# token = request.GET.get("token")
# # token = request.GET.get("token") or request.COOKIES.get("jwt")
# print("token: ", token)
# if token:
# try:
# # Decode and validate token
# payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
# print("payload: ", payload)
# try:
# UserModel = get_user_model()
# user = UserModel.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:
# # Handle expired token
# return HttpResponseBadRequest("No Principal Found")
# except jwt.ExpiredSignatureError:
# # Handle expired token
# return HttpResponseBadRequest("Expired Signature Error")
# except jwt.InvalidTokenError:
# return HttpResponseBadRequest("Invalid Token Error")
# return super().get(request, *args, **kwargs)
class SubscriptionCancelFailsView(TemplateView):
template_name = "stripe_html/subscription_cancel_fails.html"

View File

@@ -71,7 +71,7 @@ stripe==8.2.0
tqdm==4.66.2
tweepy==4.14.0
Twisted==23.10.0
twisted-iocpsupport==1.0.4
#twisted-iocpsupport==1.0.4
txaio==23.1.1
typing_extensions==4.9.0
tzdata==2024.1

View File

@@ -818,4 +818,29 @@ div#accordionExample {
.footer .store-app {
margin-bottom: 16px;
}
}
.coupon-code-input,
.common-btn {
display: inline-block;
margin-right: 10px;
}
.common-btn {
/* remove default button margins */
margin: 0;
}
.feat-card input.form-control.coupon-code-input {
border-radius: 5px;
width: 100%;
background: transparent;
color: #fff;
margin-bottom: 20px;
margin-right: 0;
caret-color: #fff;
text-align: center;
}
.feat-card input.form-control.coupon-code-input::placeholder {
color: #a5a4a4;
}

View File

@@ -154,6 +154,17 @@
</a>
</li>
{% endif %}
{% 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">
<div class="">
<span class="material-symbols-outlined">local_offer</span>
<span>Manage Coupons</span>
</div>
</a>
</li>
{% endif %}
{% 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

@@ -16,7 +16,7 @@
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>Add Stocks</h3>
<h3>{{operation}} {{page_name}}</h3>
</div>
<div class="col text-end">
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
@@ -30,7 +30,7 @@
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="POST">
<form method="POST" novalidate>
{% csrf_token %}
{% include 'includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
@@ -67,69 +67,3 @@
{% 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

@@ -0,0 +1,149 @@
{% 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 Coupons</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_coupons:coupon_add' %}">Add Coupons</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;"> Code </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Used </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Max Allowed </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> From </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> To </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Amount </th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Percentage </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 coupon_obj %}
<tr role="row">
<td class="checkbox-column text-center sorting_1"> {{data_obj.id}}</td>
<td>{{data_obj.title}}</td>
<td>{{data_obj.coupon_code}}</td>
<td>{{data_obj.no_of_redeems}}</td>
<td>{{data_obj.max_redeems}}</td>
<td>{{data_obj.valid_from}}</td>
<td>{{data_obj.valid_to}}</td>
<td>{{data_obj.discount_amount}}</td>
<td>{{data_obj.discount_percentage}}</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>
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_coupons:coupon_delete' data_obj.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Delete" data-bs-original-title="Delete"
aria-label="Delete"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-trash p-1 br-8 mb-1">
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
</path>
</svg>
</a>
</li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
{% include "cdn_through_html/datatable_cdn_js.html" %}
<script>
c3 = $('#style-3').DataTable({
"dom": "<'dt--top-section'<'row'<'col-12 col-sm-6 d-flex justify-content-sm-start justify-content-center'l><'col-12 col-sm-6 d-flex justify-content-sm-end justify-content-center mt-sm-0 mt-3'f>>>" +
"<'table-responsive'tr>" +
"<'dt--bottom-section d-sm-flex justify-content-sm-between text-center'<'dt--pages-count mb-sm-0 mb-3'i><'dt--pagination'p>>",
"oLanguage": {
"oPaginate": { "sPrevious": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>', "sNext": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' },
"sInfo": "Showing page _PAGE_ of _PAGES_",
"sSearch": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
"sSearchPlaceholder": "Search...",
"sLengthMenu": "Results : _MENU_",
},
"order": [[ 0, "desc" ]],
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 10
});
multiCheck(c3);
</script>
{% endblock %}

View File

@@ -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>Add Stock Index</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">
{% 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,237 +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" %}
{% include "cdn_through_html/tabs_cdn_css.html" %}
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>Manage Stock</h3>
</div>
<div class="col text-end">
{% comment %} <button class="btn btn-dark mb-2 me-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button> {% endcomment %}
<a class="btn btn-success mb-2 me-4" href="{% url 'manage_stock:stock_index_add' %}">Add Stock Index</a>
<a class="btn btn-primary mb-2 me-4" href="{% url 'manage_stock:stock_add' %}">Add Stock</a>
</div>
</div>
<div class="row">
<div id="tabsSimple" class="col-xl-12 col-12 layout-spacing">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<div class="simple-pill">
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-category-tab" data-bs-toggle="pill" data-bs-target="#pills-category" type="button" role="tab" aria-controls="pills-category" aria-selected="false">Stock Index List</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-new-article-tab" data-bs-toggle="pill" data-bs-target="#pills-new-article" type="button" role="tab" aria-controls="pills-new-article" aria-selected="true">Stock List</button>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade" id="pills-category" role="tabpanel" aria-labelledby="pills-category-tab" tabindex="0">
<div id="category_wrapper" class="dataTables_wrapper container-fluid dt-bootstrap4 no-footer">
<div class="table-responsive">
<table id="category" 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="sorting" tabindex="0" aria-controls="style-3"
colspan="1"
style="width: 44.2344px;">Category Name</th>
<th class="sorting" tabindex="0" aria-controls="style-3"
style="width: 79.7969px;">Active</th>
<th class="dt-no-sorting sorting" tabindex="0"
aria-controls="style-3"
style="width: 51.625px;">Action</th>
</tr>
</thead>
<tbody>
{% for category in stock_index_obj %}
<tr role="row">
<td class="checkbox-column text-center sorting_1"> {{category.id}}</td>
<td>{{category.title}}</td>
<td class="text-center">
<span class="shadow-none badge {% if category.active %}badge-primary{% else %}badge-danger{% endif %}">{{category.active}}</span>
</td>
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_stock:stock_index_edit' category.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Edit" data-bs-original-title="Edit"
aria-label="Edit"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-edit-2 p-1 br-8 mb-1">
<path
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
</path>
</svg></a></li>
<li><a href="{% url 'manage_stock:stock_index_delete' category.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Delete" data-bs-original-title="Delete"
aria-label="Delete"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-trash p-1 br-8 mb-1">
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
</path>
</svg></a></li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade active show" id="pills-new-article" role="tabpanel" aria-labelledby="pills-new-article-tab" tabindex="0">
<div id="new-article_wrapper" class="dataTables_wrapper container-fluid dt-bootstrap4 no-footer">
<div class="table-responsive">
<table id="new-article" 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 text-center sorting_asc" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1" aria-sort="ascending"
style="width: 69.2656px;"> Record Id </th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
colspan="1"
style="width: 44.2344px;">Category</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 79.7969px;">Title</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 79.7969px;">Active</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 51.625px;">Action</th>
</tr>
</thead>
<tbody>
{% for data_obj in stock_obj %}
<tr role="row">
<td class="checkbox-column text-center sorting_1">{{data_obj.id}}</td>
<td>{{data_obj.index_type.title}}</td>
<td>{{data_obj.title}}</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>
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_stock:stock_edit' data_obj.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Edit" data-bs-original-title="Edit"
aria-label="Edit"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-edit-2 p-1 br-8 mb-1">
<path
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
</path>
</svg></a></li>
<li><a href="{% url 'manage_stock:stock_delete' data_obj.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Delete" data-bs-original-title="Delete"
aria-label="Delete"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-trash p-1 br-8 mb-1">
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
</path>
</svg></a></li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</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>
category = $('#category').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_",
},
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 10
});
new_article = $('#new-article').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_",
},
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 10
});
</script>
{% endblock %}

View File

@@ -1,175 +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" %}
{% include "cdn_through_html/tabs_cdn_css.html" %}
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>Manage Stock</h3>
</div>
<div class="col text-end">
{% comment %} <button class="btn btn-dark mb-2 me-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button> {% endcomment %}
<a class="btn btn-success mb-2 me-4" href="{% url 'manage_stock:stock_index_add' %}">Add Stock Index</a>
<a class="btn btn-primary mb-2 me-4" href="{% url 'manage_stock:stock_add' %}">Add Stock</a>
</div>
</div>
<div class="row">
<div id="tabsSimple" class="col-xl-12 col-12 layout-spacing">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<div class="simple-pill">
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-category-tab" data-bs-toggle="pill" data-bs-target="#pills-category" type="button" role="tab" aria-controls="pills-category" aria-selected="false">Stock Index List</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-new-article-tab" data-bs-toggle="pill" data-bs-target="#pills-new-article" type="button" role="tab" aria-controls="pills-new-article" aria-selected="true">Stock List</button>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade" id="pills-category" role="tabpanel" aria-labelledby="pills-category-tab" tabindex="0">
<div id="category_wrapper" class="dataTables_wrapper container-fluid dt-bootstrap4 no-footer">
<div class="table-responsive">
<table id="category" 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="sorting" tabindex="0" aria-controls="style-3"
colspan="1"
style="width: 44.2344px;">Category Name</th>
<th class="sorting" tabindex="0" aria-controls="style-3"
style="width: 79.7969px;">Active</th>
<th class="dt-no-sorting sorting" tabindex="0"
aria-controls="style-3"
style="width: 51.625px;">Action</th>
</tr>
</thead>
<tbody>
{% for category in stock_price_obj %}
<tr role="row">
<td class="checkbox-column text-center sorting_1"> {{category.id}}</td>
<td>{{category.title}}</td>
<td class="text-center">
<span class="shadow-none badge {% if category.active %}badge-primary{% else %}badge-danger{% endif %}">{{category.active}}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade active show" id="pills-new-article" role="tabpanel" aria-labelledby="pills-new-article-tab" tabindex="0">
<div id="new-article_wrapper" class="dataTables_wrapper container-fluid dt-bootstrap4 no-footer">
<div class="table-responsive">
<table id="new-article" 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 text-center sorting_asc" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1" aria-sort="ascending"
style="width: 69.2656px;"> Record Id </th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
colspan="1"
style="width: 44.2344px;">Category</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 79.7969px;">Title</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 51.625px;">Action</th>
</tr>
</thead>
<tbody>
{% for data_obj in stock_price_obj %}
<tr role="row">
<td class="checkbox-column text-center sorting_1">{{data_obj.id}}</td>
<td>{{data_obj.index_type.title}}</td>
<td>{{data_obj.title}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</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>
category = $('#category').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_",
},
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 10
});
new_article = $('#new-article').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_",
},
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 10
});
</script>
{% endblock %}

View File

@@ -1,156 +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>{{data_objs.0.team.title}}</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>
-->
{% comment %} <a class="btn btn-primary mb-2 me-md-4" href="{% url 'manage_games:game_entryfee_add' %}">Example form</a> {% endcomment %}
{% comment %} <a class="btn btn-primary mb-2" href="{% url 'manage_games:game_entryfee_add' %}">Add Entry Fee</a> {% endcomment %}
</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 text-center sorting_asc" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1" aria-sort="ascending"
style="width: 69.2656px;"> Record Id </th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Team</th> -->
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Stock</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Quantity</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Operation</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Position</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Last Closing</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Current Closing</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Current Closing Percentage</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
style="width: 44.2344px;">Stock Score</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 98.875px;">Active</th>
<!-- <th class="text-center dt-no-sorting" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1"
style="width: 51.625px;">Action</th> -->
</tr>
</thead>
<tbody>
{% for data_obj in data_objs %}
<tr role="row">
<!-- <td class="checkbox-column text-center sorting_1"> {{ data_obj.id }} </td>
<td>{{ data_obj.team }}</td> -->
<td>{{ data_obj.stock }}</td>
<td>{{ data_obj.quantity }}</td>
<td>{{ data_obj.get_operation_display }}</td>
<td>{{ data_obj.get_position_display }}</td>
<td>{{ data_obj.last_closing }}</td>
<td>{{ data_obj.current_closing }}</td>
<td>{{ data_obj.current_closing_percentage }}</td>
<td>{{ data_obj.stock_score }}</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>
<!-- <td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_games:game_entryfee_edit' data_obj.id%}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Edit" data-bs-original-title="Edit"
aria-label="Edit"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-edit-2 p-1 br-8 mb-1">
<path
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
</path>
</svg></a></li>
<li><a href="javascript:void(0);" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Delete" data-bs-original-title="Delete"
aria-label="Delete"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-trash p-1 br-8 mb-1">
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
</path>
</svg></a></li>
</ul>
</td> -->
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
{% include "cdn_through_html/datatable_cdn_js.html" %}
<script>
c3 = $('#style-3').DataTable({
"dom": "<'dt--top-section'<'row'<'col-12 col-sm-6 d-flex justify-content-sm-start justify-content-center'l><'col-12 col-sm-6 d-flex justify-content-sm-end justify-content-center mt-sm-0 mt-3'f>>>" +
"<'table-responsive'tr>" +
"<'dt--bottom-section d-sm-flex justify-content-sm-between text-center'<'dt--pages-count mb-sm-0 mb-3'i><'dt--pagination'p>>",
"oLanguage": {
"oPaginate": { "sPrevious": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>', "sNext": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' },
"sInfo": "Showing page _PAGE_ of _PAGES_",
"sSearch": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
"sSearchPlaceholder": "Search...",
"sLengthMenu": "Results : _MENU_",
},
"stripeClasses": [],
"lengthMenu": [5, 10, 20, 50],
"pageLength": 20
});
multiCheck(c3);
</script>
{% endblock %}

View File

@@ -0,0 +1,99 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block content %}
<div class="container mt-5">
<div class="row">
<!-- Principal Subscription Title and Image -->
<div class="col-md-12 mb-4">
<div class="card shadow-sm">
<div class="card-body text-center">
{% if principal_subscription_obj.subscription.image %}
<img src="{{ principal_subscription_obj.subscription.image.url }}" class="img-fluid rounded mb-3" alt="{{ principal_subscription_obj.subscription.title }}" style="max-height: 200px;">
{% endif %}
<h3 class="card-title">{{ principal_subscription_obj.subscription.title }}</h3>
</div>
</div>
</div>
<!-- Subscription and Principal Info -->
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100 bg-dark text-light border-gold">
<div class="card-header border-bottom-gold">
<h5 class="card-title text-gold"><i class="bi bi-info-circle"></i> Subscription Info</h5>
</div>
<div class="card-body">
<p><strong>Principal:</strong> {{ principal_subscription_obj.principal.first_name }} {{ principal_subscription_obj.principal.last_name }}</p>
<p><strong>Status:</strong> {{ principal_subscription_obj.get_status_display }}</p>
<p><strong>Start Date:</strong> {{ principal_subscription_obj.start_date }}</p>
<p><strong>End Date:</strong> {{ principal_subscription_obj.end_date }}</p>
<p><strong>Auto Renew:</strong> {{ principal_subscription_obj.auto_renew|yesno:"Yes,No" }}</p>
<p><strong>Cancelled:</strong> {% if principal_subscription_obj.cancelled %}<span class="badge bg-danger">Yes</span>{% else %}<span class="badge bg-success">No</span>{% endif %}</p>
{% if principal_subscription_obj.coupon_code %}
<p><strong>Coupon Code:</strong> {{ principal_subscription_obj.coupon_code }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Cancellation and Payment Info -->
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100 bg-dark text-light border-gold">
<div class="card-header border-bottom-gold">
<h5 class="card-title text-gold"><i class="bi bi-credit-card"></i> Cancellation and Payment Info</h5>
</div>
<div class="card-body">
<p><strong>Order ID:</strong> {{ principal_subscription_obj.order_id|default:"Not Provided" }}</p>
<p><strong>Grace Period Ends:</strong> {{ principal_subscription_obj.grace_period_end_date|default:"Not Provided" }}</p>
<p><strong>Stripe Subscription ID:</strong> {{ principal_subscription_obj.stripe_subscription_id|default:"Not Provided" }}</p>
<p><strong>Comments:</strong> {{ principal_subscription_obj.comments|default:"Not Provided" }}</p>
</div>
</div>
</div>
<!-- Cancellation Button -->
{% if principal_subscription_obj.auto_renew and not principal_subscription_obj.cancelled %}
<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' %}">
{% 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>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock content %}
<style>
.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>

View File

@@ -51,12 +51,18 @@
style="width: 69.2656px;"> Is Paid </th>
<th class="sorting" tabindex="7" aria-controls="style-3"
style="width: 79.7969px;">Status</th>
<th class="dt-no-sorting sorting" tabindex="8"
aria-controls="style-3"
style="width: 100.625px;">Stripe ID</th>
<th class="dt-no-sorting sorting" tabindex="8"
aria-controls="style-3"
style="width: 100.625px;">Start</th>
<th class="dt-no-sorting sorting" tabindex="8"
aria-controls="style-3"
style="width: 100.625px;">End</th>
<th class="checkbox-column sorting_asc" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 69.2656px;"> Is Subscription </th>
<th class="dt-no-sorting sorting" tabindex="8"
aria-controls="style-3"
style="width: 100.625px;">Grace</th>
@@ -79,8 +85,12 @@
<td>{{data_obj.principal}}</td>
<td>{{data_obj.is_paid}}</td>
<td>{{data_obj.status}}</td>
<td>{{data_obj.stripe_subscription_id}}</td>
<td>{{data_obj.start_date}}</td>
<td>{{data_obj.end_date}}</td>
<td class="text-center">
<span class="shadow-none badge {% if data_obj.is_stripe_subscription %}badge-primary{% else %}badge-danger{% endif %}">{{data_obj.is_stripe_subscription}}</span>
</td>
<td>{{data_obj.grace_period_end_date}}</td>
<td>{{data_obj.created_on}}</td>
<td class="text-center">
@@ -100,19 +110,13 @@
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
</path>
</svg></a></li>
<!-- <li><a href="{% url 'manage_subscriptions:principal_subscription_delete' data_obj.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Delete" data-bs-original-title="Delete"
aria-label="Delete"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-trash p-1 br-8 mb-1">
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
</path>
</svg></a></li> -->
<li>
<a href="{% url 'manage_subscriptions:principal_subscription_detail' data_obj.id %}">
<span class="material-symbols-outlined">
visibility
</span>
</a>
</li>
</ul>
</td>
</tr>

View File

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

View File

@@ -0,0 +1,104 @@
{% 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

@@ -16,7 +16,7 @@
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>{{operation}} {{page_name}}</h3>
<h3>Add Subscription</h3>
</div>
<div class="col text-end">
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
@@ -67,69 +67,3 @@
{% 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

@@ -0,0 +1,77 @@
{% extends 'layout/base_template.html' %}
{% load static %}
{% block content %}
<div class="container mt-5">
<div class="row">
<!-- Subscription Title and Image -->
<div class="col-md-12 mb-4">
<div class="card shadow-sm">
<div class="card-body text-center">
{% if principal_subscription_obj.image %}
<img src="{{ subscription.image.url }}" class="img-fluid rounded mb-3" alt="{{ subscription.title }}" style="max-height: 200px;">
{% endif %}
<h3 class="card-title">{{ subscription.title }}</h3>
</div>
</div>
</div>
<!-- Plan and Pricing Info -->
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header text-white">
<h5 class="card-title"><i class="bi bi-currency-dollar"></i> Plan & Pricing</h5>
</div>
<div class="card-body">
<p><strong>Plan:</strong> {{ subscription.plan.title }}</p>
<p><strong>Price ID:</strong> {{ subscription.price_id|default:"Not a Stripe Subscription" }}</p>
<p><strong>Stripe Product:</strong> {{ subscription.stripe_product|default:"None" }}</p>
<p><strong>Amount:</strong> ${{ subscription.amount }}</p>
<p><strong>High Amount:</strong> ${{ subscription.high_amount }}</p>
<p><strong>Referral Percentage:</strong> {{ subscription.referral_percentage }}%</p>
<p><strong>Is Free:</strong>
{% if subscription.is_free %}
<span class="badge bg-success">Yes</span>
{% else %}
<span class="badge bg-danger">No</span>
{% endif %}
</p>
</div>
</div>
</div>
<!-- Descriptions -->
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header text-dark">
<h5 class="card-title"><i class="bi bi-file-earmark-text"></i> Descriptions</h5>
</div>
<div class="card-body">
<p><strong>Short Description:</strong> {{ subscription.short_description|default:"Not Provided" }}</p>
<p><strong>Long Description:</strong></p>
<p>{{ subscription.long_description|default:"Not Provided" }}</p>
</div>
</div>
</div>
<!-- Principal Types -->
<div class="col-md-12 mb-4">
<div class="card shadow-sm">
<div class="card-header text-white">
<h5 class="card-title"><i class="bi bi-people"></i> Principal Types</h5>
</div>
<div class="card-body">
<ul class="list-group">
{% for principal_type in subscription.principal_types.all %}
<li class="list-group-item">{{ principal_type.name }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -21,7 +21,9 @@
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>
@@ -56,6 +58,9 @@
<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"
@@ -83,23 +88,38 @@
<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>
<td class="text-center">
<ul class="table-controls">
<li><a href="{% url 'manage_subscriptions:subscription_edit' data_obj.id %}" class="bs-tooltip"
<li><a href="{% url 'manage_subscriptions:subscription_delete' data_obj.id %}" class="bs-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title=""
data-original-title="Edit" data-bs-original-title="Edit"
aria-label="Edit"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-edit-2 p-1 br-8 mb-1">
<path
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
</path>
</svg>
data-original-title="Edit" data-bs-original-title="Delete"
aria-label="Delete"><svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="feather feather-trash p-1 br-8 mb-1">
<polyline points="3 6 5 6 21 6"></polyline>
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
</path>
</svg>
</a>
</li>
<li>
<a href="{% url 'manage_subscriptions:subscription_detail' data_obj.id %}">
<span class="material-symbols-outlined">
visibility
</span>
</a>
</li>
</ul>

View File

@@ -0,0 +1,158 @@
<!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>
<header class="text-center py-3">
<h1 class="text-gold">Your Active Subscription</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>
</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>
{% if active_subscription.coupon_code %}
<p><strong>Coupon Code:</strong> {{ active_subscription.coupon_code }}</p>
{% endif %}
{% if active_subscription.cancelled %}
<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>
</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>
{% endif %}
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,17 +1,17 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Good times</title>
{% load static %}
<link rel="icon" type="image/x-icon" href="{% static '/images/icon.png' %}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" />
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<link rel="stylesheet" href="{% static 'src/assets/css/payment/style.css' %}">
<title>Good times</title>
<script src="https://js.stripe.com/v3/"></script>
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/5.3.2/css/bootstrap.min.css" rel="stylesheet">
@@ -128,15 +128,24 @@
</div>
<div class="Adventure-btn text-center">
<input type="text" placeholder="Enter Coupon Code" class="form-control coupon-code-input" size="20">
<!-- 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
</label>
</div>
<!-- Add a data attribute to store subscription ID -->
<button class="common-btn subscribe-btn" data-subscription-id="{{ subscription.id }}">Join
now</button>
<button class="common-btn subscribe-btn" data-subscription-id="{{ subscription.id }}" data-price-id="{{ subscription.price_id }}">Join now</button>
<!-- Error message container -->
<div class="alert alert-danger coupon-error-message mt-2" style="display: none;"></div>
</div>
</div>
</div>
<hr>
{% empty %}
<p>No subscriptions available.</p>
{% endfor %}
@@ -517,50 +526,96 @@
console.log("Sanity check!");
var stripeCheckoutUrl = "{{ stripeCheckoutUrl }}";
var stripeFinalUrl = "{{ stripeFinalUrl }}";
var couponValidityCheckUrl = "{{ couponValidityCheckUrl }}";
console.log("stripeCheckoutUrl: ", stripeCheckoutUrl);
console.log("stripeFinalUrl: ", stripeFinalUrl);
// Get Stripe publishable key
console.log("couponValidityCheckUrl: ", couponValidityCheckUrl);
// Geting Stripe publishable key
fetch(stripeCheckoutUrl)
.then((result) => { return result.json(); })
.then((result) => {
return result.json();
})
.then((data) => {
// Initialize Stripe.js
// 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;
// Create checkout session for the selected subscription
fetch(stripeFinalUrl, {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ subscriptionId: subscriptionId }),
})
.then((result) => { return result.json(); })
.then((data) => {
// Redirect to Stripe Checkout
return stripe.redirectToCheckout({ sessionId: data.sessionId })
button.previousElementSibling.value = "";
// 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: recurringCheckbox.checked
}),
})
.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',
},
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;
});
})
.catch((error) => {
console.error("Error:", error);
// const errorMessageElement = document.getElementById('already-active-subscription');
// if (errorMessageElement) {
// errorMessageElement.innerText = "Error: " + error.message; // Display the error in the specific div
// errorMessageElement.style.color = 'red'; // Set text color to red
// errorMessageElement.style.fontWeight = 'bold'; // Set text to bold
// }
errorMessageContainer.style.display = 'block';
errorMessageContainer.innerText = error.message;
button.disabled = false;
});
});
});
});
</script>

View File

@@ -1,9 +1,11 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="/static/images/icon.png">
<title>Goodtimes</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v6.4.0/js/all.js"></script>
@@ -22,7 +24,7 @@
console.log("Sanity check!");
// Get Stripe publishable key
fetch("https://goodtimes.betadelivery.com/subscriptions/stripe-subscription/")
fetch("http://localhost:8000/subscriptions/stripe-subscription/")
.then((result) => { return result.json(); })
.then((data) => {
// Initialize Stripe.js
@@ -32,7 +34,7 @@
// Event handler
document.querySelector("#submitBtn").addEventListener("click", () => {
// Get Checkout Session ID
fetch("https://goodtimes.betadelivery.com/subscriptions/create-checkout-session/")
fetch("http://localhost:8000/subscriptions/create-checkout-session/")
.then((result) => { return result.json(); })
.then((data) => {
console.log(data);

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Goodtimes</title>
</head>
<body>
<h3>
Subscription Cancellation Failed
</h3>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Goodtimes</title>
</head>
<body>
<h3>
Subscription Successfully Cancelled
</h3>
</body>
</html>