@@ -137,19 +137,15 @@ 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)
|
||||
email = serializers.CharField(read_only=True)
|
||||
invite_count = serializers.SerializerMethodField(read_only=True)
|
||||
principal_type_name = serializers.SerializerMethodField(read_only=True)
|
||||
has_active_subscription = serializers.SerializerMethodField(read_only=True)
|
||||
has_preferences = serializers.SerializerMethodField(read_only=True)
|
||||
register_complete = serializers.BooleanField(read_only=True)
|
||||
email = serializers.CharField(read_only=True)
|
||||
is_active = serializers.BooleanField(read_only=True)
|
||||
going_events_count = serializers.SerializerMethodField(read_only=True)
|
||||
interested_events_count = serializers.SerializerMethodField(read_only=True)
|
||||
phone_no = serializers.CharField(required=True)
|
||||
|
||||
class Meta:
|
||||
@@ -163,18 +159,12 @@ class ProfileSerializer(serializers.ModelSerializer):
|
||||
"last_name",
|
||||
"phone_no",
|
||||
"email",
|
||||
"invite_count",
|
||||
"register_complete",
|
||||
"has_active_subscription",
|
||||
"has_preferences",
|
||||
"linkedin_profile",
|
||||
"youtube_profile",
|
||||
"facebook_profile",
|
||||
"instagram_profile",
|
||||
"website",
|
||||
"is_active",
|
||||
"going_events_count",
|
||||
"interested_events_count",
|
||||
]
|
||||
|
||||
# def validate_phone_no(self, value):
|
||||
@@ -196,71 +186,14 @@ class ProfileSerializer(serializers.ModelSerializer):
|
||||
instance.last_name = validated_data.get("last_name", instance.last_name)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def get_going_events_count(self, obj):
|
||||
return EventPrincipalInteraction.objects.filter(
|
||||
principal=obj, status="going"
|
||||
).count()
|
||||
|
||||
def get_interested_events_count(self, obj):
|
||||
return EventPrincipalInteraction.objects.filter(
|
||||
principal=obj, status="interested"
|
||||
).count()
|
||||
|
||||
def get_invite_count(self, obj):
|
||||
if obj:
|
||||
return ReferralRecord.get_invite_count(obj)
|
||||
return 0
|
||||
|
||||
def get_principal_type_name(self, obj):
|
||||
return obj.principal_type.name if obj.principal_type else None
|
||||
|
||||
def get_has_preferences(self, obj):
|
||||
return PrincipalPreference.objects.filter(principal=obj).exists()
|
||||
|
||||
def get_image_url(self, obj, field_name, request):
|
||||
image_field = getattr(obj, field_name)
|
||||
if image_field:
|
||||
return request.build_absolute_uri(image_field.url)
|
||||
return ""
|
||||
|
||||
def get_has_active_subscription(self, obj):
|
||||
subscription_status = {
|
||||
"has_active_subscription": False,
|
||||
"in_grace_period": False,
|
||||
"grace_period_end_date": None,
|
||||
}
|
||||
today = timezone.now().date()
|
||||
|
||||
# Attempt to find the active subscription with the furthest grace_period_end_date
|
||||
latest_subscription = (
|
||||
PrincipalSubscription.objects.filter(
|
||||
principal=obj,
|
||||
is_paid=True,
|
||||
cancelled=False,
|
||||
deleted=False,
|
||||
active=True,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
)
|
||||
.order_by("-grace_period_end_date")
|
||||
.first()
|
||||
) # Order by descending grace_period_end_date and take the first
|
||||
|
||||
if latest_subscription:
|
||||
# Check if we're within the grace period
|
||||
if today <= latest_subscription.grace_period_end_date:
|
||||
subscription_status["has_active_subscription"] = (
|
||||
today <= latest_subscription.end_date
|
||||
)
|
||||
subscription_status["in_grace_period"] = (
|
||||
latest_subscription.end_date
|
||||
< today
|
||||
<= latest_subscription.grace_period_end_date
|
||||
)
|
||||
subscription_status["grace_period_end_date"] = (
|
||||
latest_subscription.grace_period_end_date
|
||||
)
|
||||
|
||||
return subscription_status
|
||||
def get_principal_type_name(self, obj):
|
||||
return obj.principal_type.name if obj.principal_type else None
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
@@ -273,8 +206,7 @@ class ProfileExtendedDataSerializer(serializers.ModelSerializer):
|
||||
principal_type_name = serializers.SerializerMethodField(read_only=True)
|
||||
has_active_subscription = serializers.SerializerMethodField(read_only=True)
|
||||
preference = serializers.SerializerMethodField(read_only=True)
|
||||
register_complete = serializers.BooleanField(read_only=True)
|
||||
is_active = serializers.BooleanField(read_only=True)
|
||||
principal_preference_count = serializers.SerializerMethodField(read_only=True)
|
||||
going_events_count = serializers.SerializerMethodField(read_only=True)
|
||||
interested_events_count = serializers.SerializerMethodField(read_only=True)
|
||||
feature_limit = serializers.SerializerMethodField(read_only=True)
|
||||
@@ -287,7 +219,7 @@ class ProfileExtendedDataSerializer(serializers.ModelSerializer):
|
||||
"register_complete",
|
||||
"has_active_subscription",
|
||||
"preference",
|
||||
"is_active",
|
||||
"principal_preference_count",
|
||||
"going_events_count",
|
||||
"interested_events_count",
|
||||
"feature_limit"
|
||||
@@ -314,11 +246,19 @@ class ProfileExtendedDataSerializer(serializers.ModelSerializer):
|
||||
def get_preference(self, obj):
|
||||
return PrincipalPreference.objects.filter(principal=obj).exists()
|
||||
|
||||
def get_principal_preference_count(self, obj):
|
||||
principal_preference = PrincipalPreference.objects.filter(principal=obj).first()
|
||||
if principal_preference:
|
||||
categories = principal_preference.preferred_categories.all()
|
||||
return categories.count()
|
||||
return 0
|
||||
|
||||
def get_has_active_subscription(self, obj):
|
||||
subscription_status = {
|
||||
"has_active_subscription": False,
|
||||
"in_grace_period": False,
|
||||
"grace_period_end_date": None,
|
||||
"subscription_id": None,
|
||||
}
|
||||
today = timezone.now().date()
|
||||
|
||||
@@ -328,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"] = (
|
||||
@@ -343,7 +286,7 @@ class ProfileExtendedDataSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
return subscription_status
|
||||
|
||||
|
||||
def get_feature_limit(self, obj):
|
||||
from manage_events.api.serializers import FreeUsageFeatureLimitSerializer
|
||||
obj = FreeUsageFeatureLimit.objects.first()
|
||||
@@ -450,4 +393,4 @@ class AppVersionSerializer(serializers.ModelSerializer):
|
||||
class IAmPrincipalExtendedDataSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = IAmPrincipalExtendedData
|
||||
fields = "__all__"
|
||||
fields = "__all__"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -364,7 +364,12 @@ class IAmPrincipalResourceLinkForm(IAmPrincipalForm):
|
||||
class CreateCustomerForm(forms.Form):
|
||||
first_name = forms.CharField(max_length=255, required=True, label='First Name')
|
||||
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
|
||||
business_name = forms.CharField(max_length=200, required=True, label="Business Name")
|
||||
email = forms.EmailField(required=True, label='Email')
|
||||
phone_no = PhoneNumberField(
|
||||
widget=forms.TextInput(),
|
||||
label="Phone No"
|
||||
)
|
||||
preferences = forms.ModelMultipleChoiceField(
|
||||
queryset=EventCategory.objects.all(),
|
||||
widget=forms.widgets.SelectMultiple(
|
||||
@@ -383,6 +388,14 @@ class CreateCustomerForm(forms.Form):
|
||||
label=_('Free period end date'),
|
||||
help_text=_('Enter the end date of the free period')
|
||||
)
|
||||
address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40}))
|
||||
city = forms.CharField(max_length=200, required=False, label="Region")
|
||||
country = forms.CharField(max_length=200, required=False, label="Country")
|
||||
website = forms.URLField(max_length=255, required=False, label="Website")
|
||||
linkedin_profile = forms.URLField(max_length=200, required=False, label="LinkedIn")
|
||||
facebook_profile = forms.URLField(max_length=200, required=False, label="Facebook")
|
||||
instagram_profile = forms.URLField(max_length=200, required=False, label="Instagram")
|
||||
twitter_profile = forms.URLField(max_length=200, required=False, label="Twitter")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -391,7 +404,12 @@ class CreateCustomerForm(forms.Form):
|
||||
class UpdateCustomerForm(forms.Form):
|
||||
first_name = forms.CharField(max_length=255, required=True, label='First Name')
|
||||
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
|
||||
business_name = forms.CharField(max_length=200, required=True, label="Business Name")
|
||||
email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'}))
|
||||
phone_no = PhoneNumberField(
|
||||
widget=forms.TextInput(),
|
||||
label="Phone No"
|
||||
)
|
||||
preferences = forms.ModelMultipleChoiceField(
|
||||
queryset=EventCategory.objects.all(),
|
||||
widget=forms.widgets.SelectMultiple(
|
||||
@@ -410,6 +428,14 @@ class UpdateCustomerForm(forms.Form):
|
||||
label=_('Free period end date'),
|
||||
help_text=_('Enter the end date of the free period')
|
||||
)
|
||||
address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40}))
|
||||
city = forms.CharField(max_length=200, required=False, label="Region")
|
||||
country = forms.CharField(max_length=200, required=False, label="Country")
|
||||
website = forms.URLField(max_length=255, required=False, label="Website")
|
||||
linkedin_profile = forms.URLField(max_length=200, required=False, label="LinkedIn")
|
||||
facebook_profile = forms.URLField(max_length=200, required=False, label="Facebook")
|
||||
instagram_profile = forms.URLField(max_length=200, required=False, label="Instagram")
|
||||
twitter_profile = forms.URLField(max_length=200, required=False, label="Twitter")
|
||||
active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
18
accounts/migrations/0014_iamprincipal_business_name.py
Normal file
18
accounts/migrations/0014_iamprincipal_business_name.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-08-11 16:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0013_iamprincipalextendeddata_pwd_changed_post_transfer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='iamprincipal',
|
||||
name='business_name',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Business Name'),
|
||||
),
|
||||
]
|
||||
18
accounts/migrations/0015_iamprincipal_twitter_profile.py
Normal file
18
accounts/migrations/0015_iamprincipal_twitter_profile.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-08-12 08:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0014_iamprincipal_business_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='iamprincipal',
|
||||
name='twitter_profile',
|
||||
field=models.URLField(blank=True, max_length=255, null=True, verbose_name='Principal Twitter'),
|
||||
),
|
||||
]
|
||||
@@ -320,6 +320,8 @@ class IAmPrincipal(AbstractUser):
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
business_name = models.CharField(verbose_name="Business Name", max_length=200, blank=True, null=True)
|
||||
twitter_profile = models.URLField(verbose_name="Principal Twitter", max_length=255, null=True, blank=True)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -627,11 +627,21 @@ class CustomerCreateView(LoginRequiredMixin, generic.View):
|
||||
email=form.cleaned_data.get('email'),
|
||||
first_name=form.cleaned_data.get('first_name'),
|
||||
last_name=form.cleaned_data.get('last_name'),
|
||||
business_name=form.cleaned_data.get('business_name'),
|
||||
phone_no=form.cleaned_data.get('phone_no'),
|
||||
password=make_password("goodtimes#2024"),
|
||||
username=form.cleaned_data.get("email"),
|
||||
email_verified=True,
|
||||
register_complete=True,
|
||||
principal_type=IAmPrincipalType.objects.get(name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER),
|
||||
address_line1=form.cleaned_data.get("address_line1"),
|
||||
city=form.cleaned_data.get("city"),
|
||||
country=form.cleaned_data.get("country"),
|
||||
website=form.cleaned_data.get("website"),
|
||||
linkedin_profile=form.cleaned_data.get("linkedin_profile"),
|
||||
facebook_profile=form.cleaned_data.get("facebook_profile"),
|
||||
instagram_profile=form.cleaned_data.get("instagram_profile"),
|
||||
twitter_profile=form.cleaned_data.get("twitter_profile"),
|
||||
)
|
||||
|
||||
# generate referralcode of manager
|
||||
@@ -648,7 +658,7 @@ class CustomerCreateView(LoginRequiredMixin, generic.View):
|
||||
principal_preference = PrincipalPreference.objects.create(principal=principal_obj)
|
||||
principal_preference.preferred_categories.set(form.cleaned_data.get("preferences"))
|
||||
|
||||
principal_subscription= PrincipalSubscription.objects.create(
|
||||
principal_subscription = PrincipalSubscription.objects.create(
|
||||
start_date=form.cleaned_data.get("free_start_date"),
|
||||
end_date=form.cleaned_data.get("free_end_date"),
|
||||
principal=principal_obj,
|
||||
@@ -660,6 +670,7 @@ class CustomerCreateView(LoginRequiredMixin, generic.View):
|
||||
messages.success(self.request, constants.REGISTRATION_SUCCESS)
|
||||
return redirect(self.success_url)
|
||||
except Exception as e:
|
||||
print("errror is ", e)
|
||||
messages.error(self.request, str(e))
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
@@ -685,12 +696,28 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
principal_id = kwargs.get("pk")
|
||||
principal_obj = IAmPrincipal.objects.get(pk=principal_id)
|
||||
try:
|
||||
principal_obj = IAmPrincipal.objects.get(pk=principal_id)
|
||||
except Exception as e:
|
||||
messages.error(request, f"No Record of id {principal_id} is found")
|
||||
return redirect(self.success_url)
|
||||
|
||||
print(f"principal address is {principal_obj.address_line1}")
|
||||
|
||||
initial_data = {
|
||||
"first_name": principal_obj.first_name,
|
||||
"last_name": principal_obj.last_name,
|
||||
"email": principal_obj.email,
|
||||
"business_name": principal_obj.business_name,
|
||||
"phone_no": principal_obj.phone_no,
|
||||
"address_line1": principal_obj.address_line1,
|
||||
"city": principal_obj.city,
|
||||
"country": principal_obj.country,
|
||||
"website": principal_obj.website,
|
||||
"linkedin_profile": principal_obj.linkedin_profile,
|
||||
"facebook_profile": principal_obj.facebook_profile,
|
||||
"instagram_profile": principal_obj.instagram_profile,
|
||||
"twitter_profile": principal_obj.twitter_profile,
|
||||
"active": principal_obj.is_active
|
||||
}
|
||||
|
||||
@@ -714,8 +741,12 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
customer_id = kwargs.get("pk")
|
||||
principal_obj = IAmPrincipal.objects.get(pk=customer_id)
|
||||
principal_id = kwargs.get("pk")
|
||||
try:
|
||||
principal_obj = IAmPrincipal.objects.get(pk=principal_id)
|
||||
except Exception as e:
|
||||
messages.error(request, f"No Record of customer id {principal_id} is found")
|
||||
return redirect(self.success_url)
|
||||
form = self.form_class(request.POST)
|
||||
if not form.is_valid():
|
||||
context = self.get_context_data(form=form)
|
||||
@@ -725,6 +756,16 @@ class CustomerUpdateView(LoginRequiredMixin, generic.View):
|
||||
# update principal data
|
||||
principal_obj.first_name = form.cleaned_data.get('first_name')
|
||||
principal_obj.last_name = form.cleaned_data.get('last_name')
|
||||
principal_obj.business_name = form.cleaned_data.get("business_name")
|
||||
principal_obj.phone_no = form.cleaned_data.get("phone_no")
|
||||
principal_obj.address_line1 = form.cleaned_data.get("address_line1")
|
||||
principal_obj.city = form.cleaned_data.get("city")
|
||||
principal_obj.country = form.cleaned_data.get("country")
|
||||
principal_obj.website = form.cleaned_data.get("website")
|
||||
principal_obj.linkedin_profile = form.cleaned_data.get("linkedin_profile")
|
||||
principal_obj.facebook_profile = form.cleaned_data.get("facebook_profile")
|
||||
principal_obj.instagram_profile = form.cleaned_data.get("instagram_profile")
|
||||
principal_obj.twitter_profile = form.cleaned_data.get("twitter_profile")
|
||||
principal_obj.save()
|
||||
|
||||
# update principal preferences record
|
||||
@@ -881,26 +922,75 @@ from django.http import HttpResponse
|
||||
# wb.save(response)
|
||||
# return response
|
||||
|
||||
|
||||
# from openpyxl.styles import Font
|
||||
def export_excel_template(request):
|
||||
# Define the columns and create an empty DataFrame
|
||||
columns = ['First Name', 'Last Name', 'Email', 'Preferences(should be seperated by comma)', 'Free period start date(YYYY-MM-DD)', 'Free period end date(YYYY-MM-DD)']
|
||||
# Define the columns for the Customer Registration sheet
|
||||
columns = [
|
||||
'First Name',
|
||||
'Last Name',
|
||||
'Business Name',
|
||||
'Email',
|
||||
'Phone No',
|
||||
'Preferences (should be separated by comma)',
|
||||
'Free period start date (YYYY-MM-DD)',
|
||||
'Free period end date (YYYY-MM-DD)',
|
||||
'Address',
|
||||
'Region',
|
||||
'Country',
|
||||
'Website',
|
||||
'LinkedIn',
|
||||
'Facebook',
|
||||
'Instagram',
|
||||
'Twitter',
|
||||
]
|
||||
df = pd.DataFrame(columns=columns)
|
||||
|
||||
# Create a workbook and select the active worksheet
|
||||
# Create a workbook and add the Customer Registration worksheet
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = 'Customer Registration'
|
||||
ws_customer = wb.active
|
||||
ws_customer.title = 'Manager Onboarding'
|
||||
|
||||
# Write the column headers
|
||||
# Write the column headers for the Customer Registration sheet
|
||||
for col_num, column_title in enumerate(df.columns, 1):
|
||||
cell = ws.cell(row=1, column=col_num, value=column_title)
|
||||
cell = ws_customer.cell(row=1, column=col_num, value=column_title)
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
# Create the Readme worksheet
|
||||
ws_readme = wb.create_sheet(title='Readme')
|
||||
|
||||
# Add information about each field to the Readme sheet
|
||||
readme_data = [
|
||||
['Field Name', 'Description'],
|
||||
['First Name', 'The first name of the customer. This is a required field.'],
|
||||
['Last Name', 'The last name of the customer. This is a required field.'],
|
||||
['Business Name', 'The official name of the customer\'s business or organization.'],
|
||||
['Email', 'The email address of the customer. This must be a unique email not already used in the system. This is a required Field'],
|
||||
['Phone No', 'The business phone number. It should include the country code if applicable.'],
|
||||
['Category', 'A comma-separated list of event categories the customer is interested in. These should match existing categories in the system. This is a required Field'],
|
||||
['Free period start date', 'The start date of the customer\'s free trial period, formatted as YYYY-MM-DD.'],
|
||||
['Free period end date', 'The end date of the customer\'s free trial period, formatted as YYYY-MM-DD. This date must be later than the start date.'],
|
||||
['Address', 'The complete business address, including street, city, and postal code.'],
|
||||
['Region', 'The geographic region where the business operates.'],
|
||||
['Country', 'The country where the business is based.'],
|
||||
['Website', 'The URL of the business\'s official website. Ensure it includes "http://" or "https://".'],
|
||||
['LinkedIn', 'The LinkedIn profile URL of the business. Ensure it includes "http://" or "https://".'],
|
||||
['Facebook', 'The Facebook page URL of the business. Ensure it includes "http://" or "https://".'],
|
||||
['Instagram', 'The Instagram profile URL of the business. Ensure it includes "http://" or "https://".'],
|
||||
['Twitter', 'The Twitter handle or profile URL of the business. Ensure it includes "http://" or "https://".'],
|
||||
]
|
||||
|
||||
# Write the Readme data to the Readme worksheet
|
||||
for row_num, row_data in enumerate(readme_data, 1):
|
||||
for col_num, cell_value in enumerate(row_data, 1):
|
||||
cell = ws_readme.cell(row=row_num, column=col_num, value=cell_value)
|
||||
if row_num == 1: # Make the header bold
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
# Save the workbook to a bytes buffer
|
||||
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
response['Content-Disposition'] = 'attachment; filename=customer_registration_template.xlsx'
|
||||
wb.save(response)
|
||||
|
||||
return response
|
||||
|
||||
class CustomerTransferView(LoginRequiredMixin, generic.View):
|
||||
@@ -921,7 +1011,7 @@ class CustomerTransferView(LoginRequiredMixin, generic.View):
|
||||
|
||||
# Send the email
|
||||
try:
|
||||
temp_password="goodtimes#2024"
|
||||
temp_password = "goodtimes#2024"
|
||||
principal_obj.password = make_password(temp_password)
|
||||
principal_obj.save()
|
||||
email_service.load_template(
|
||||
@@ -968,12 +1058,19 @@ class CustomerImportView(LoginRequiredMixin, generic.View):
|
||||
excel_file = request.FILES['file']
|
||||
|
||||
wb = load_workbook(filename=excel_file)
|
||||
ws = wb.active
|
||||
|
||||
# Check if the specific sheet exists
|
||||
if 'Manager Onboarding' not in wb.sheetnames:
|
||||
form.add_error('file', 'The required sheet "Manager Onboarding" is not present in the uploaded file.')
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
# Load the "Manager Onboarding" worksheet
|
||||
ws = wb['Manager Onboarding']
|
||||
|
||||
error_log = []
|
||||
|
||||
principals = []
|
||||
preferences_l = []
|
||||
preferences_list = []
|
||||
subscriptions = []
|
||||
principal_type = IAmPrincipalType.objects.get(name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER)
|
||||
free_subscription = Subscription.objects.filter(is_free=True, active=True).first()
|
||||
@@ -983,7 +1080,7 @@ class CustomerImportView(LoginRequiredMixin, generic.View):
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
for idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
|
||||
first_name, last_name, email, preferences, start_date, end_date = row
|
||||
first_name, last_name, business_name, email, phone_no, preferences, start_date, end_date, address, region, country, website, linkedin, facebook, instagram, twitter = row
|
||||
print(f"{first_name}, {last_name, email, preferences, start_date, end_date}")
|
||||
|
||||
# validate all data
|
||||
@@ -1017,16 +1114,26 @@ class CustomerImportView(LoginRequiredMixin, generic.View):
|
||||
username=email.strip(),
|
||||
email_verified=True,
|
||||
register_complete=True,
|
||||
principal_type=principal_type
|
||||
principal_type=principal_type,
|
||||
business_name=business_name,
|
||||
phone_no=str(phone_no),
|
||||
address_line1=address,
|
||||
city=region,
|
||||
country=country,
|
||||
website=website,
|
||||
linkedin_profile=linkedin,
|
||||
facebook_profile=facebook,
|
||||
instagram_profile=instagram,
|
||||
twitter_profile=twitter
|
||||
)
|
||||
principals.append(principal)
|
||||
|
||||
# Collect preferences to be set later
|
||||
preferences_l.append((principal, event_categories, start_date, end_date))
|
||||
preferences_list.append((principal, event_categories, start_date, end_date))
|
||||
|
||||
if error_log:
|
||||
context = self.get_context_data(form=form, error_log=error_log)
|
||||
messages.error(request, "No recore is created check error log and fix the error in the file ")
|
||||
messages.error(request, "No record is created check error log and fix the error in the file ")
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
# Use transaction.atomic to ensure all-or-nothing
|
||||
@@ -1039,7 +1146,7 @@ class CustomerImportView(LoginRequiredMixin, generic.View):
|
||||
principals = IAmPrincipal.objects.filter(email__in=[p.email for p in principals])
|
||||
|
||||
# Create subscriptions and preferences
|
||||
for principal, event_categories, start_date, end_date in preferences_l:
|
||||
for principal, event_categories, start_date, end_date in preferences_list:
|
||||
principal = principals.get(email=principal.email)
|
||||
|
||||
# Generate referral code for the manager
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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
|
||||
# )
|
||||
0
goodtimes/webhook/__init__.py
Normal file
0
goodtimes/webhook/__init__.py
Normal file
80
goodtimes/webhook/notification_service.py
Normal file
80
goodtimes/webhook/notification_service.py
Normal 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
|
||||
180
goodtimes/webhook/payment_processing_service.py
Normal file
180
goodtimes/webhook/payment_processing_service.py
Normal 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()
|
||||
111
goodtimes/webhook/referral_reward_service.py
Normal file
111
goodtimes/webhook/referral_reward_service.py
Normal 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)
|
||||
77
goodtimes/webhook/subscription_service.py
Normal file
77
goodtimes/webhook/subscription_service.py
Normal 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!!!")
|
||||
90
goodtimes/webhook/webhook_service.py
Normal file
90
goodtimes/webhook/webhook_service.py
Normal 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
|
||||
)
|
||||
0
manage_coupons/__init__.py
Normal file
0
manage_coupons/__init__.py
Normal file
40
manage_coupons/admin.py
Normal file
40
manage_coupons/admin.py
Normal 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)
|
||||
0
manage_coupons/api/serializers.py
Normal file
0
manage_coupons/api/serializers.py
Normal file
0
manage_coupons/api/urls.py
Normal file
0
manage_coupons/api/urls.py
Normal file
0
manage_coupons/api/views.py
Normal file
0
manage_coupons/api/views.py
Normal file
6
manage_coupons/apps.py
Normal file
6
manage_coupons/apps.py
Normal 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
24
manage_coupons/forms.py
Normal 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'}),
|
||||
}
|
||||
81
manage_coupons/migrations/0001_initial.py
Normal file
81
manage_coupons/migrations/0001_initial.py
Normal 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",
|
||||
},
|
||||
),
|
||||
]
|
||||
18
manage_coupons/migrations/0002_coupon_coupon_id.py
Normal file
18
manage_coupons/migrations/0002_coupon_coupon_id.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-07-31 07:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("manage_coupons", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="coupon",
|
||||
name="coupon_id",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
0
manage_coupons/migrations/__init__.py
Normal file
0
manage_coupons/migrations/__init__.py
Normal file
79
manage_coupons/models.py
Normal file
79
manage_coupons/models.py
Normal 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
3
manage_coupons/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
23
manage_coupons/urls.py
Normal file
23
manage_coupons/urls.py
Normal 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
47
manage_coupons/utils.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import stripe
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
def handle_stripe_coupon(coupon_instance, stripe_secret_key):
|
||||
"""
|
||||
Handles the creation or updating of a Stripe coupon.
|
||||
Returns True if successful, otherwise returns False.
|
||||
"""
|
||||
try:
|
||||
stripe.api_key = stripe_secret_key
|
||||
|
||||
# Prepare coupon data without setting the ID
|
||||
coupon_data = {
|
||||
"name": coupon_instance.title,
|
||||
"metadata": {
|
||||
"local_id": coupon_instance.id,
|
||||
},
|
||||
"redeem_by": int(coupon_instance.valid_to.timestamp()),
|
||||
"max_redemptions": (
|
||||
coupon_instance.max_redeems if coupon_instance.max_redeems > 0 else None
|
||||
),
|
||||
"duration": "once",
|
||||
}
|
||||
|
||||
if coupon_instance.discount_amount:
|
||||
coupon_data["amount_off"] = int(
|
||||
coupon_instance.discount_amount * Decimal(100)
|
||||
) # Amount in cents/fils
|
||||
coupon_data["currency"] = "gbp"
|
||||
elif coupon_instance.discount_percentage:
|
||||
coupon_data["percent_off"] = float(coupon_instance.discount_percentage)
|
||||
|
||||
# Creating a new Stripe coupon
|
||||
stripe_coupon = stripe.Coupon.create(**coupon_data)
|
||||
# Using the Stripe-generated ID for coupon_code and coupon_id
|
||||
coupon_instance.coupon_code = stripe_coupon.id
|
||||
coupon_instance.coupon_id = stripe_coupon.id
|
||||
|
||||
# Saving the coupon instance after successful Stripe operation
|
||||
coupon_instance.save()
|
||||
return True, "Coupon successfully created."
|
||||
|
||||
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
137
manage_coupons/views.py
Normal 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)
|
||||
@@ -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")
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenRefreshView,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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}),
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -35,10 +35,18 @@
|
||||
<div class="col-md-3">Last Name</div>
|
||||
<div class="col-md-9">{{principal_obj.last_name}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Business Name</div>
|
||||
<div class="col-md-9">{{principal_obj.business_name}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Email Address</div>
|
||||
<div class="col-md-9">{{principal_obj.email}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Phone No</div>
|
||||
<div class="col-md-9">{{principal_obj.phone_no}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Preferences</div>
|
||||
<div class="col-md-9">
|
||||
@@ -61,6 +69,38 @@
|
||||
<div class="col-md-3">End Date</div>
|
||||
<div class="col-md-9">{% if principal_subscription %}{{ principal_subscription.end_date }}{% else %}No subscription found{% endif %}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Address</div>
|
||||
<div class="col-md-9">{{principal_obj.address_line1}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Region</div>
|
||||
<div class="col-md-9">{{principal_obj.city}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Country</div>
|
||||
<div class="col-md-9">{{principal_obj.country}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Website</div>
|
||||
<div class="col-md-9">{{principal_obj.website}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Facebook</div>
|
||||
<div class="col-md-9">{{principal_obj.facebook_profile}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">LinkedIn</div>
|
||||
<div class="col-md-9">{{principal_obj.linkedin_profile}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Instagram</div>
|
||||
<div class="col-md-9">{{principal_obj.instagram_profile}}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">Twitter</div>
|
||||
<div class="col-md-9">{{principal_obj.twitter_profile}}</div>
|
||||
</div>
|
||||
{% if principal_obj.extended_data and not principal_obj.extended_data.is_transferred and principal_obj.extended_data.is_onboarded and principal_obj.principal_type.name == 'event_manager' %}
|
||||
<div class="col text-end">
|
||||
<a class="btn btn-dark mb-2 me-4" href="{% url 'accounts:customer_transfer' principal_obj.id %}">
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
</td> -->
|
||||
<td class="text-center">
|
||||
<ul class="table-controls">
|
||||
{% if data_obj.extended_data.is_onboarded%}
|
||||
<li><a href="{% url 'accounts:customer_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"
|
||||
@@ -141,6 +142,7 @@
|
||||
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z">
|
||||
</path>
|
||||
</svg></a></li>
|
||||
{%endif%}
|
||||
<li><a href="{% url 'accounts:customer_detail' data_obj.id%}" class="bs-tooltip" data-bs-toggle="tooltip" data-bs-placement="top" title="" data-original-title="View" data-bs-original-title="View" aria-label="View">
|
||||
<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-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
</a></li>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 %}
|
||||
149
templates/manage_coupons/coupon_list.html
Normal file
149
templates/manage_coupons/coupon_list.html
Normal 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 %}
|
||||
@@ -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 & Drop your files or <span class="filepond--label-action" tabindex="0">Browse</span></label></div><div class="filepond--list-scroller" style="transform: translate3d(0px, 41px, 0px);"><ul class="filepond--list" role="list"></ul></div><div class="filepond--panel filepond--panel-root" data-scalable="true"><div class="filepond--panel-top filepond--panel-root"></div><div class="filepond--panel-center filepond--panel-root" style="transform: translate3d(0px, 8px, 0px) scale3d(1, 0.41, 1);"></div><div class="filepond--panel-bottom filepond--panel-root" style="transform: translate3d(0px, 49px, 0px);"></div></div><span class="filepond--assistant" id="filepond--assistant-feeq8o6dj" role="status" aria-live="polite" aria-relevant="additions"></span><div class="filepond--drip"></div><fieldset class="filepond--data"></fieldset></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="tags">Tags</label>
|
||||
<input id="tags" class="tags" value="">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button> {% endcomment %}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
|
||||
{% include "cdn_through_html/filepond_cdn_js.html" %}
|
||||
{% include "cdn_through_html/quill_cdn_js.html" %}
|
||||
{% include "cdn_through_html/tagify_cdn_js.html" %}
|
||||
|
||||
|
||||
<script>
|
||||
/**
|
||||
* ===================================
|
||||
* Blog Description Editor
|
||||
* ===================================
|
||||
*/
|
||||
var quill = new Quill('#description', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block']
|
||||
]
|
||||
},
|
||||
placeholder: 'Write description...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* ====================
|
||||
* File Pond
|
||||
* ====================
|
||||
*/
|
||||
|
||||
// We want to preview images, so we register
|
||||
// the Image Preview plugin, We also register
|
||||
// exif orientation (to correct mobile image
|
||||
// orientation) and size validation, to prevent
|
||||
// large files from being added
|
||||
FilePond.registerPlugin(
|
||||
FilePondPluginImagePreview,
|
||||
FilePondPluginImageExifOrientation,
|
||||
FilePondPluginFileValidateSize,
|
||||
// FilePondPluginImageEdit
|
||||
);
|
||||
|
||||
// Select the file input and use
|
||||
// create() to turn it into a pond
|
||||
var ecommerce = FilePond.create(document.querySelector('.file-upload-multiple'));
|
||||
|
||||
|
||||
/**
|
||||
* =====================
|
||||
* Blog Tags
|
||||
* =====================
|
||||
*/
|
||||
// The DOM element you wish to replace with Tagify
|
||||
var input = document.querySelector('#id_tags');
|
||||
|
||||
// initialize Tagify on the above input node reference
|
||||
new Tagify(input,{
|
||||
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(', ')
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
49
templates/manage_subscriptions/product_add.html
Normal file
49
templates/manage_subscriptions/product_add.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends 'layout/base_template.html' %}
|
||||
{% load static %}
|
||||
{% block stylesheet %}
|
||||
<!-- include required css cdn link through html here -->
|
||||
|
||||
{% include "cdn_through_html/filepond_cdn_css.html" %}
|
||||
{% include "cdn_through_html/quill_cdn_css.html" %}
|
||||
{% include "cdn_through_html/tagify_cdn_css.html" %}
|
||||
{{form.media}}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row layout-top-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<h3>Add Product</h3>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row layout-spacing">
|
||||
<div class="col-lg-12">
|
||||
<div class="statbox widget box box-shadow">
|
||||
<div class="widget-content widget-content-area">
|
||||
|
||||
<form method="POST" novalidate>
|
||||
{% csrf_token %}
|
||||
{% include 'includes/dynamic_template_form.html' with form=form %}
|
||||
<div class="mt-4 mb-0">
|
||||
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
104
templates/manage_subscriptions/product_list.html
Normal file
104
templates/manage_subscriptions/product_list.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
77
templates/manage_subscriptions/subscription_details.html
Normal file
77
templates/manage_subscriptions/subscription_details.html
Normal 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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
158
templates/stripe_html/active_subscription.html
Normal file
158
templates/stripe_html/active_subscription.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
13
templates/stripe_html/subscription_cancel_fails.html
Normal file
13
templates/stripe_html/subscription_cancel_fails.html
Normal 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>
|
||||
13
templates/stripe_html/subscription_cancel_success.html
Normal file
13
templates/stripe_html/subscription_cancel_success.html
Normal 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>
|
||||
Reference in New Issue
Block a user