diff --git a/accounts/context_processors.py b/accounts/context_processors.py index 98689de..0eecf0f 100644 --- a/accounts/context_processors.py +++ b/accounts/context_processors.py @@ -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, diff --git a/accounts/fixture_script.py b/accounts/fixture_script.py index 663ee5b..921f26d 100644 --- a/accounts/fixture_script.py +++ b/accounts/fixture_script.py @@ -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], + }, + }, ] diff --git a/accounts/fixtures/resource_action_fixture.json b/accounts/fixtures/resource_action_fixture.json index df2e808..e48d27f 100644 --- a/accounts/fixtures/resource_action_fixture.json +++ b/accounts/fixtures/resource_action_fixture.json @@ -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 + ] + } } ] \ No newline at end of file diff --git a/accounts/resource_action.py b/accounts/resource_action.py index 3e10751..7bad7a7 100644 --- a/accounts/resource_action.py +++ b/accounts/resource_action.py @@ -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 diff --git a/goodtimes/settings/development.py b/goodtimes/settings/development.py index cd26290..0fc2e13 100644 --- a/goodtimes/settings/development.py +++ b/goodtimes/settings/development.py @@ -53,5 +53,6 @@ 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" diff --git a/goodtimes/settings/production.py b/goodtimes/settings/production.py index c3ed2bf..edd145c 100644 --- a/goodtimes/settings/production.py +++ b/goodtimes/settings/production.py @@ -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" diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index 315c495..ade2303 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -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" diff --git a/goodtimes/urls.py b/goodtimes/urls.py index 031d221..c31e7f0 100644 --- a/goodtimes/urls.py +++ b/goodtimes/urls.py @@ -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")), diff --git a/goodtimes/webhook.py b/goodtimes/webhook.py index 19e9633..5f1481a 100644 --- a/goodtimes/webhook.py +++ b/goodtimes/webhook.py @@ -5,6 +5,7 @@ from datetime import timedelta from django.utils import timezone from onesignal_sdk.client import Client as OneSignalClient from accounts.models import IAmPrincipal +from manage_coupons.models import Coupon from manage_notifications.models import ( IAmPrincipalNotificationSettings, InAppNotification, @@ -132,6 +133,16 @@ class WebhookService: def get_order_id(self): return self.charge_data["metadata"]["order_id"] + def get_coupon(self): + coupon_code = self.charge_data["metadata"].get("couponCode") + 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 + class ReferralRewardService: def __init__(self, principal, principal_subscription, subscription): @@ -225,7 +236,7 @@ class SubscriptionService: def __init__(self): self.principal_subscription = None - def create_principal_subscription(self, principal, subscription, order_id): + def create_principal_subscription(self, principal, subscription, order_id, coupon=None): subscription_days = subscription.plan.days today = timezone.now().date() last_date = today + timedelta(days=subscription_days) @@ -237,7 +248,11 @@ class SubscriptionService: start_date=today, end_date=last_date, grace_period_end_date=last_date + timedelta(days=15), + coupon_code=coupon.coupon_code if coupon else None, ) + if coupon: + coupon.no_of_redeems += 1 + coupon.save() self.principal_subscription = principal_subscription return principal_subscription @@ -260,6 +275,7 @@ class PaymentProcessingService: self.transaction = self.webhook_service.get_transaction() self.subscription = self.webhook_service.get_subscription() self.order_id = self.webhook_service.get_order_id() + self.coupon = self.webhook_service.get_coupon() self.subscription_service = SubscriptionService() self.principal_subscription = None @@ -274,7 +290,7 @@ class PaymentProcessingService: # Create or update the principal subscription self.principal_subscription = ( self.subscription_service.create_principal_subscription( - self.principal, self.subscription, self.order_id + self.principal, self.subscription, self.order_id, self.coupon ) ) print("First Part Done....!!!!!") diff --git a/manage_coupons/forms.py b/manage_coupons/forms.py index e69de29..c89a25e 100644 --- a/manage_coupons/forms.py +++ b/manage_coupons/forms.py @@ -0,0 +1,47 @@ +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", + "coupon_code", + "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"}), + } + + def clean(self): + cleaned_data = super().clean() + discount_amount = cleaned_data.get("discount_amount") + discount_percentage = cleaned_data.get("discount_percentage") + valid_from = cleaned_data.get("valid_from") + valid_to = cleaned_data.get("valid_to") + + if discount_amount and discount_percentage: + raise ValidationError( + "You can only set either a discount amount or a discount percentage, not both." + ) + + if not discount_amount and not discount_percentage: + raise ValidationError( + "You must set either a discount amount or a discount percentage." + ) + + if valid_from and valid_to and valid_from >= valid_to: + raise ValidationError( + "The valid_from date must be earlier than the valid_to date." + ) + + return cleaned_data diff --git a/manage_coupons/migrations/0001_initial.py b/manage_coupons/migrations/0001_initial.py new file mode 100644 index 0000000..728c884 --- /dev/null +++ b/manage_coupons/migrations/0001_initial.py @@ -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", + }, + ), + ] diff --git a/manage_coupons/models.py b/manage_coupons/models.py index 46c267f..f38dad6 100644 --- a/manage_coupons/models.py +++ b/manage_coupons/models.py @@ -2,12 +2,11 @@ from django.db import models from django.utils import timezone from accounts.models import BaseModel, IAmPrincipalType -# Create your models here. - class Coupon(BaseModel): 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(null=True, blank=True) image = models.ImageField(upload_to="coupon_img", null=True, blank=True) discount_amount = models.DecimalField( @@ -16,11 +15,9 @@ class Coupon(BaseModel): discount_percentage = models.DecimalField( max_digits=5, decimal_places=2, null=True, blank=True ) - principal_types = models.ManyToManyField( - IAmPrincipalType, related_name="principal_type_coupons", blank=True - ) valid_from = models.DateTimeField() valid_to = models.DateTimeField() + max_redeems = models.IntegerField(default=0) class Meta: db_table = "coupon" @@ -28,6 +25,13 @@ class Coupon(BaseModel): 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 + 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) + ) diff --git a/manage_coupons/urls.py b/manage_coupons/urls.py index e69de29..9c3001c 100644 --- a/manage_coupons/urls.py +++ b/manage_coupons/urls.py @@ -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//", + views.CouponCreateOrUpdateView.as_view(), + name="coupon_edit", + ), + path( + "coupon/delete//", + views.CouponDeleteView.as_view(), + name="coupon_delete", + ), +] diff --git a/manage_coupons/views.py b/manage_coupons/views.py index 91ea44a..2bdb0e4 100644 --- a/manage_coupons/views.py +++ b/manage_coupons/views.py @@ -1,3 +1,114 @@ -from django.shortcuts import render +from django.shortcuts import get_object_or_404, render, redirect +from django.views import generic +from django.contrib.auth.mixins import LoginRequiredMixin +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 # 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) + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) + + +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) + type_obj.deleted = True + type_obj.active = False + type_obj.save() + messages.success(request, self.success_message) + except self.model.DoesNotExist: + messages.success(request, self.error_message) + + return redirect(self.success_url) diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index 79ed3ac..65701fa 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -1,4 +1,5 @@ from django import forms +from accounts.models import IAmPrincipalType from manage_subscriptions.models import PrincipalSubscription, Subscription, Plan @@ -31,7 +32,15 @@ class SubscriptionForm(forms.ModelForm): "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] + ) class PrincipalSubscriptionForm(forms.ModelForm): diff --git a/manage_subscriptions/migrations/0009_principalsubscription_coupon_code.py b/manage_subscriptions/migrations/0009_principalsubscription_coupon_code.py new file mode 100644 index 0000000..b693abb --- /dev/null +++ b/manage_subscriptions/migrations/0009_principalsubscription_coupon_code.py @@ -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), + ), + ] diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index 326f7df..4e8e1da 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -71,6 +71,7 @@ class PrincipalSubscription(BaseModel): 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" diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index aa767f8..20210bf 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -70,6 +70,11 @@ 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("success/", views.SuccessView.as_view(), name="success"), path("cancel/", views.CancelView.as_view(), name="cancel"), diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index 7a91d56..17e5f8d 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -1,18 +1,16 @@ -from datetime import timedelta +from decimal import Decimal import json -from django.http import HttpResponseBadRequest, JsonResponse, HttpResponseRedirect +from django.http import HttpResponseBadRequest, 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, SubscriptionForm, PrincipalSubscriptionForm, ) @@ -24,7 +22,6 @@ from manage_wallets.models import ( ) from .models import Plan, Subscription, PrincipalSubscription 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.contrib import messages @@ -32,12 +29,10 @@ 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 # Create your views here. -from django.db.models import Q class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource @@ -101,7 +96,10 @@ 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) @@ -119,7 +117,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): @@ -395,15 +398,19 @@ class SubscriptionPageView(TemplateView): 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 + 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: - # Handle the case where no subscriptions are found for the principal type. + # Handling the case where no subscriptions are found for the principal type. context["error"] = "No subscriptions found for your user type." return context @@ -420,18 +427,37 @@ def stripe_config(request): def validate_coupon(request): data = json.loads(request.body) coupon_code = data.get("couponCode", None) + subscription_id = data.get("subscriptionId", None) - # Validate coupon code + try: + subscription = Subscription.objects.get(id=subscription_id) + except Subscription.DoesNotExist: + return JsonResponse({"error": "Subscription not found."}, status=404) + + # Validating Coupon if coupon_code: try: - coupon = stripe.Coupon.retrieve(coupon_code) - if coupon["valid"] is False: - return JsonResponse({"error": "Invalid or expired coupon code."}, status=400) - return JsonResponse({"success": "Coupon code is valid."}) - except stripe.error.InvalidRequestError: - return JsonResponse({"error": "Invalid coupon code."}, status=400) - else: - return JsonResponse({"error": "Coupon code not provided."}, status=400) + coupon = Coupon.objects.get(coupon_code=coupon_code) + if not coupon.is_valid(): + return JsonResponse({"error": "Coupon is not valid."}, status=400) + if coupon.discount_amount and coupon.discount_amount > subscription.amount: + return JsonResponse( + {"error": "Coupon discount amount exceeds subscription amount."}, + status=400, + ) + if ( + coupon.discount_percentage + and (coupon.discount_percentage / Decimal("100")) * subscription.amount + > subscription.amount + ): + return JsonResponse( + { + "error": "Coupon discount percentage exceeds subscription amount." + }, + status=400, + ) + except Coupon.DoesNotExist: + return JsonResponse({"error": "Coupon not found."}, status=404) @csrf_exempt @@ -442,6 +468,7 @@ def create_checkout_session(request): data = json.loads(request.body) print("data: ", data) subscription_id = data.get("subscriptionId", None) + coupon_code = data.get("couponCode", None) principal_id = request.user.id try: @@ -453,7 +480,28 @@ def create_checkout_session(request): "order_" + str(timezone.localtime().timestamp()) + str(request.user.email) ) print("order_id: ", order_id) - + # Calculating the final amount after applying the coupon discount + final_amount = subscription.amount + coupon = None + if coupon_code: + try: + coupon = Coupon.objects.get(coupon_code=coupon_code) + if coupon.is_valid(): + if coupon.discount_amount: + final_amount -= coupon.discount_amount + elif coupon.discount_percentage: + final_amount -= final_amount * ( + coupon.discount_percentage / Decimal("100") + ) + final_amount = max( + 0, final_amount + ) # Ensuring the amount is not negative + else: + return JsonResponse( + {"error": "Invalid or expired coupon code."}, status=400 + ) + except Coupon.DoesNotExist: + return JsonResponse({"error": "Coupon not found."}, status=404) # Create a Transaction object with status INITIATE transaction = Transaction.objects.create( principal=request.user, @@ -461,7 +509,7 @@ def create_checkout_session(request): 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 + amount=final_amount, # Fetching amount from the Subscription object order_id=order_id, comment="Principal Subscription Initiated", ) @@ -492,7 +540,7 @@ def create_checkout_session(request): "name": subscription.title, # Adjust with your subscription/product name }, "unit_amount": int( - subscription.amount * 100 + final_amount * 100 ), # Unit amount in cents/pence "tax_behavior": "inclusive", # or 'exclusive', based on your tax settings }, @@ -509,6 +557,7 @@ def create_checkout_session(request): "order_id": str(order_id), "subscription_id": str(subscription.id), "transaction_id": str(transaction.id), + "couponCode": str(coupon.coupon_code) if coupon else None, # "principal_subscription_id": str(principal_subscription.id), }, ) @@ -523,38 +572,3 @@ class SuccessView(TemplateView): class CancelView(TemplateView): template_name = "stripe_html/cancel.html" - - -# class IndexView(TemplateView): -# template_name = "stripe_html/index.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) diff --git a/templates/elements/sidebar.html b/templates/elements/sidebar.html index bd845d0..a493e8a 100644 --- a/templates/elements/sidebar.html +++ b/templates/elements/sidebar.html @@ -154,6 +154,17 @@ {% endif %} + {% if user|has_resource_permission:resource_context.RESOURCE_MANAGE_COUPONS %} + + {% endif %} {% if user|has_resource_permission:resource_context.RESOURCE_PRINCIPAL_SUBSCRIPTIONS %}