diff --git a/goodtimes/services.py b/goodtimes/services.py index 6488f7e..f27fc41 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -1,6 +1,8 @@ import random import requests import googlemaps +import stripe +import stripe.error import tweepy from django.conf import settings from django.core.files.uploadedfile import UploadedFile @@ -13,12 +15,7 @@ from django.db.models import Case, When from smtplib import SMTPException from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType -from manage_referrals.models import ( - GoodTimeCoins, - ReferralRecord, - ReferralRecordReward, - ReferralTracking, -) + from manage_subscriptions.models import PrincipalSubscription, Subscription from manage_wallets.models import ( TransactionStatus, @@ -208,195 +205,6 @@ class SMSService: # self.send(phone_numbers, body) return otp_code - -class PaymentProcessingService: - def __init__(self, webhook_data): - self.webhook_data = webhook_data - self.event_type = webhook_data["type"] - self.charge_data = webhook_data["data"]["object"] - self.customer_id = self._get_customer_id() - self.transaction = self._get_transaction_by_id() - self.principal = self.transaction.principal - self.principal_subscription = None - - def _get_customer_id(self): - # Access the customer ID from the charge object - return self.charge_data.get("customer", None) - - def _get_transaction_by_id(self): - logger.debug("self.metadata: ", self.charge_data["metadata"]) - logger.debug("transaction_id: ", self.charge_data["metadata"]["transaction_id"]) - transaction_id = self.charge_data["metadata"]["transaction_id"] - if transaction_id: - try: - logger.debug("_get_transaction_by_id: ", transaction_id) - return Transaction.objects.get(id=int(transaction_id)) - except Transaction.DoesNotExist: - logger.error(f"Transaction ID {transaction_id} not found.") - return None - - def _get_subscription(self): - logger.debug( - "subscription_id: ", self.charge_data["metadata"]["subscription_id"] - ) - subscription_id = self.charge_data["metadata"]["subscription_id"] - if subscription_id: - try: - return Subscription.objects.get(id=int(subscription_id)) - except Subscription.DoesNotExist: - logger.error(f"Subscription ID {subscription_id} not found.") - return None - - def _create_principal_subscription(self): - order_id = self.charge_data["metadata"]["order_id"] - try: - subscription = self._get_subscription() - - subscription_days = subscription.plan.days - today = timezone.now().date() - last_date = today + timedelta(days=int(subscription_days)) - - principal_subscription = PrincipalSubscription.objects.create( - principal=self.principal, - subscription=subscription, - is_paid=True, - order_id=order_id, - start_date=today, - end_date=last_date, - grace_period_end_date=last_date + timedelta(days=15), - ) - self.principal_subscription = principal_subscription - return principal_subscription - except Subscription.DoesNotExist: - logger.error( - "SOmething Went Wrong inside _create_principal_subscription()." - ) - - return None - - def process_event(self): - if self.event_type == "checkout.session.completed": - self._handle_success() - else: - self._handle_failure() - - def _handle_success(self): - with transaction.atomic(): - self._create_principal_subscription() - self._update_transaction_success() - self._credit_referral_reward_if_applicable() - - def _credit_referral_reward_if_applicable(self): - # Step 1: Check for an existing, completed referral record - referral_record = ReferralRecord.objects.filter( - active=True, - deleted=False, - referred_principal_id=self.principal.id, - is_completed=True, - ).first() - - if referral_record: - # Step 2: Check for an active subscription of the referrer - today = timezone.now().date() - active_subscription = ( - PrincipalSubscription.objects.filter( - principal=referral_record.referrer_principal, - is_paid=True, - end_date__gte=today, - cancelled=False, - deleted=False, - ) - .order_by("-end_date") - .first() - ) - if active_subscription: - subscription = self._get_subscription() - if subscription: - # Calculate the reward value - percentage = ( - subscription.referral_percentage * subscription.amount / 100 - ) - - # Create a reward entry - ReferralRecordReward.objects.create( - referral_record=referral_record, - subscription=subscription, - coins=1, # Assuming this is a default or a calculated value - value=percentage, - ) - - self._credit_good_time_coin( - referral_record.referrer_principal, percentage - ) - # Here's where you call _update_reward - self._update_reward( - referral_record=referral_record, - active_subscription=active_subscription, - create_subscription_method=self.principal_subscription, - has_active_subscription=True, - ) - else: - # If there is no active subscription, still need to update reward without active_subscription - self._update_reward( - referral_record=referral_record, - active_subscription=None, - create_subscription_method=self.principal_subscription, - has_active_subscription=False, - ) - - def _credit_good_time_coin(self, referrer_principal, percentage): - # wallet, created = Wallet.objects.get_or_create(principal=referrer_principal) - # wallet.coins += 1 - # wallet.save() - Transaction.objects.create( - principal=referrer_principal, - transaction_type=TransactionType.CREDIT, - payment_method="", - transaction_status=TransactionStatus.SUCCESS, - amount=percentage, - coins=1, - comment="Referral reward", - # Populate other fields as necessary, such as `order_id`, `product_id`, or `reference_id` if applicable - ) - - def _handle_failure(self): - # Implement any necessary logic to handle a failed payment - self._update_transaction_failure() - - def _update_reward( - self, - referral_record, - active_subscription, - create_subscription_method, - has_active_subscription, - ): - # Check if the referrer has an active subscription and get its ID if it exists - referrer_subscription_id = ( - active_subscription.id if active_subscription else None - ) - - # Create a new subscription for the referred principal - referred_subscription_id = self.principal_subscription.id - - # Create or update the ReferralTracking record - ReferralTracking.objects.create( - referral_record=referral_record, - referrer_subscription_id=referrer_subscription_id, - referred_subscription_id=referred_subscription_id, - is_referrer_subscribed=has_active_subscription, - ) - - def _update_transaction_success(self): - principal_subscription = self.principal_subscription - self.transaction.transaction_status = TransactionStatus.SUCCESS - self.transaction.principal_subscription = principal_subscription - self.transaction.save() - - def _update_transaction_failure(self): - self.transaction.transaction_status = TransactionStatus.FAIL - self.transaction.save() - - class InteractionCalculator: def __init__(self, event): self.event = event @@ -708,7 +516,7 @@ class GoogleMapsservice: dict: Distance matrix response from Google Maps API. """ return self.client.distance_matrix(origin, destination) - + def search_address(self, address): """ Search for a list of addresses matching the given address string. @@ -1089,4 +897,173 @@ class InstagramPoster: result = self.instagram_api.post_image_with_caption(image_path, caption) if not result: return {'success': False, 'message': 'Error posting photo in Instagram.'} - return {'success': True, 'message': 'Photo posted successfully'} \ No newline at end of file + return {'success': True, 'message': 'Photo posted successfully'} + + +class StripeService: + stripe.api_key = settings.STRIPE_SECRET_KEY + + @staticmethod + def create_product(name: str, description: str = None, metadata: dict = None): + """ + Create a Stripe Product. + + :param name: Name of the product, meant to be displayable to the customer. + :param description: An optional description of the product. + :param metadata: An optional dictionary of key-value pairs to attach to the product. + :return: The created Stripe product object. + + See: https://docs.stripe.com/api/products/create?lang=python + """ + try: + product = stripe.Product.create(name=name, description=description, metadata=metadata) + return {'success': True, 'data': product} + except stripe.error.StripeError as e: + return {'success': False, 'message': f"Error creating product: {e}"} + + @staticmethod + def retrive_product(product_id: str): + """ + Retrieve a Stripe Product by its ID. + + :param product_id: The ID of the product to retrieve. + :return: The retrieved Stripe Product object. + + See: https://docs.stripe.com/api/products/update?lang=python + """ + try: + product = stripe.Product.retrieve(product_id) + return {'success': True, 'data': product} + except stripe.error.StripeError as e: + return {'success': False, 'message': f"Error retriving product: {e}"} + + @staticmethod + def update_product(product_id: str, **kwargs): + """ + Update a Stripe Product by its ID. + + :param product_id: The ID of the product to update. + :param kwargs: Optional paramters to update the product, such as: + - name : The new name of the product. + - description : The new description of the product. + - active : A boolean flag indicating if the product is active. + - metadata : A dictionary of key-value pairs to attach to the product. + :return: The updated Stripe Product object. + + See: https://docs.stripe.com/api/products/update?lang=python + """ + try: + product = stripe.Product.modify(product_id, **kwargs) + return {'success': True, 'data': product} + except stripe.error.StripeError as e: + return {'success': False, 'message': f"Error updating product: {e}"} + + @staticmethod + def delete_product(product_id: str): + """ + Delete a Stripe Product by its ID. + + :param product_id: ID of the product to delete. + :return: The deleted Stripe Product object. + + See: https://docs.stripe.com/api/products/delete?lang=python + """ + try: + product = stripe.Product.delete(product_id) + return {'success': True, 'data': product} + except stripe.error.StripeError as e: + return {'success': False, 'message': f"Error deleting product: {e}"} + + @staticmethod + def create_price(product_id: str = None, product_data: dict = None, unit_amount: int = None, currency: str = 'gbp', recurring: dict = None, metadata: dict = None): + """ + Create a Stripe Price for a product. + + :param product_id: ID of the product for which the price is being created. + :param product_data: A dictionary with product details to create a new product on the fly. Example: + - name : The name of the product. + - description : The description of the product. + :param unit_amount: The amount to be charged.(in cents) + :param currency: The currency of the price. + :param recurring: A dictionary with recurring pricing details. Example: + - interval : The interval at which the price is charged (e.g., 'day', 'week', 'month', 'year'. + - interval_count : The number of intervals at which the price is charged. + :param metadata: An optional dictionary of key-value pairs to attach to the price. + :return: The created Stripe Price object. + :raise ValueError: If neither product_id nor product_data is provided. + + See: https://docs.stripe.com/api/prices/create?lang=python + """ + if not product_id and not product_data: + raise ValueError("Either product_id or product_data must be provided to create a price.") + + price_data = { + 'unit_amount': unit_amount, + 'currency': currency, + 'recurring': recurring, + 'metadata': metadata + } + + if product_id: + price_data['product'] = product_id + elif product_data: + price_data['product'] = stripe.Product.create(**product_data).id + try: + price = stripe.Price.create(**price_data) + return {'success': True, 'data': price} + except stripe.error.StripeError as e: + return {'success': False, 'message': f"Error creating price: {e}"} + + @staticmethod + def retrieve_price(price_id: str): + """ + Retrieve a Stripe Price by its ID. + :param price_id: ID of the price to retrive + :return: The retrieved Stripe Price object + + See: https://docs.stripe.com/api/prices/retrieve?lang=python + """ + try: + price = stripe.Price.retrieve(price_id) + return {'success': True, 'data': price} + except stripe.error.StripeError as e: + return {'success': False, 'message': f"Error retrieving price: {e}"} + + @staticmethod + def update_price(price_id: str, **kwargs): + """ + Update a Stripe Price by its ID. + :param price_id: ID of the price to update + :param kwargs: Optional parameters to update the price, such as: + - active: A boolean flag indicating if the price is active. + - nickname: A nickname for the price, useful for labeling and organizing. + - metadata: A set of key-value pairs to attach to the price object. + :return: The updated Stripe Price object + + See: https://docs.stripe.com/api/prices/update?lang=python + """ + try: + price = stripe.Price.modify(price_id, **kwargs) + return {'success': True, 'data': price} + except stripe.error.StripeError as e: + return {'success': False, 'message': f"Error updating price: {e}"} + + # stipe not provide to delete the price + + @staticmethod + def cancel_auto_renew_subscription(subscription_id: str) -> dict: + """ + Cancels the auto-renewal of a Stripe subscription. + + :param subscription_id: The ID of the subscription to cancel auto-renewal for. + :return: A dictionary with success status and the updated subscription object or an error message. + """ + try: + # Update the subscription to cancel at the end of the current period + subscription = stripe.Subscription.modify( + subscription_id, + cancel_at_period_end=True + ) + return {'success': True, 'data': subscription} + except stripe.error.StripeError as e: + return {'success': False, 'message': f'Error cancelling subscription auto-renewal: {e}'} \ No newline at end of file diff --git a/goodtimes/settings/development.py b/goodtimes/settings/development.py index 0fc2e13..42b58d3 100644 --- a/goodtimes/settings/development.py +++ b/goodtimes/settings/development.py @@ -51,8 +51,7 @@ STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR.joinpath("static")] -STRIPE_CHECKOUT_URL = "http://localhost:8000/subscriptions/stripe-subscription/" -STRIPE_FINAL_URL = "http://localhost:8000/subscriptions/create-checkout-session/" -COUPON_VALIDITY_CHECK_URL = "http://localhost:8000/subscriptions/coupon-validity-check/" +STRIPE_CHECKOUT_URL = "https://c2f5-122-179-140-110.ngrok-free.app/subscriptions/create-checkout-session/" +COUPON_VALIDITY_CHECK_URL = "https://c2f5-122-179-140-110.ngrok-free.app/subscriptions/coupon-validity-check/" LOGO_PATH = "static" diff --git a/goodtimes/settings/production.py b/goodtimes/settings/production.py index edd145c..9efa105 100644 --- a/goodtimes/settings/production.py +++ b/goodtimes/settings/production.py @@ -77,9 +77,6 @@ STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = ( - "https://admin.goodtimesltd.co.uk/subscriptions/stripe-subscription/" -) -STRIPE_FINAL_URL = ( "https://admin.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) COUPON_VALIDITY_CHECK_URL = "https://admin.goodtimesltd.co.uk/subscriptions/coupon-validity-check/" diff --git a/goodtimes/settings/staging.py b/goodtimes/settings/staging.py index ade2303..71641cd 100644 --- a/goodtimes/settings/staging.py +++ b/goodtimes/settings/staging.py @@ -77,9 +77,6 @@ STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = ( - "https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/" -) -STRIPE_FINAL_URL = ( "https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/" ) COUPON_VALIDITY_CHECK_URL = "https://staging.goodtimesltd.co.uk/subscriptions/coupon-validity-check/" diff --git a/goodtimes/settings/wdipl.py b/goodtimes/settings/wdipl.py index 89d7593..da93a03 100644 --- a/goodtimes/settings/wdipl.py +++ b/goodtimes/settings/wdipl.py @@ -76,9 +76,6 @@ STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR.joinpath("static")] STRIPE_CHECKOUT_URL = ( - "https://goodtimes.betadelivery.com/subscriptions/stripe-subscription/" -) -STRIPE_FINAL_URL = ( "https://goodtimes.betadelivery.com/subscriptions/create-checkout-session/" ) COUPON_VALIDITY_CHECK_URL = ( diff --git a/goodtimes/webhook/referral_reward_service.py b/goodtimes/webhook/referral_reward_service.py index 6ff1257..31198c2 100644 --- a/goodtimes/webhook/referral_reward_service.py +++ b/goodtimes/webhook/referral_reward_service.py @@ -47,8 +47,7 @@ class ReferralRewardService: principal=referrer_principal, is_paid=True, end_date__gte=today, - cancelled=False, - deleted=False, + active=True, ) .order_by("-end_date") .first() diff --git a/goodtimes/webhook/subscription_service.py b/goodtimes/webhook/subscription_service.py index 68770f1..3a3f3c3 100644 --- a/goodtimes/webhook/subscription_service.py +++ b/goodtimes/webhook/subscription_service.py @@ -30,20 +30,19 @@ class SubscriptionService: ): """Create a principal subscription and return it.""" start_date, end_date = self._calculate_dates( - current_period_start, current_period_end, subscription.plan.days + current_period_start, current_period_end, subscription.calulate_days() ) principal_subscription = PrincipalSubscription.objects.create( principal=principal, subscription=subscription, - stripe_subscription_id=stripe_subscription or "Non Recurring", + stripe_subscription_id=stripe_subscription, is_paid=True, auto_renew=bool(stripe_subscription), - is_stripe_subscription=bool(stripe_subscription), order_id=order_id, start_date=start_date, end_date=end_date, - grace_period_end_date=end_date + timedelta(days=15), + grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(end_date), coupon_code=coupon.coupon_code if coupon else None, ) diff --git a/manage_subscriptions/forms.py b/manage_subscriptions/forms.py index d34f9cf..bd4a7b6 100644 --- a/manage_subscriptions/forms.py +++ b/manage_subscriptions/forms.py @@ -26,15 +26,14 @@ class SubscriptionForm(forms.ModelForm): model = Subscription fields = [ "title", - "stripe_product", - "plan", + "short_description", + "interval", + "interval_count", "high_amount", "amount", - "short_description", "principal_types", "referral_percentage", "active", - "deleted", "is_free", ] @@ -46,19 +45,6 @@ class SubscriptionForm(forms.ModelForm): id__in=[event_user.id, event_manager.id] ) - def clean(self): - cleaned_data = super().clean() - - stripe_product = cleaned_data.get("stripe_product") - - if not stripe_product: - self.add_error( - "stripe_product", - "Please select a Stripe product to create a subscription.", - ) - - return cleaned_data - class PrincipalSubscriptionForm(forms.ModelForm): class Meta: diff --git a/manage_subscriptions/migrations/0011_subscription_product_id.py b/manage_subscriptions/migrations/0011_subscription_product_id.py new file mode 100644 index 0000000..5a478b1 --- /dev/null +++ b/manage_subscriptions/migrations/0011_subscription_product_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-08-18 18:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('manage_subscriptions', '0010_principalsubscription_comments_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='subscription', + name='product_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/manage_subscriptions/migrations/0012_subscription_interval_subscription_interval_count.py b/manage_subscriptions/migrations/0012_subscription_interval_subscription_interval_count.py new file mode 100644 index 0000000..2a9979b --- /dev/null +++ b/manage_subscriptions/migrations/0012_subscription_interval_subscription_interval_count.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.2 on 2024-08-19 09:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('manage_subscriptions', '0011_subscription_product_id'), + ] + + operations = [ + migrations.AddField( + model_name='subscription', + name='interval', + field=models.CharField(choices=[('month', 'month'), ('day', 'day'), ('week', 'week'), ('year', 'year')], default='month', max_length=10), + preserve_default=False, + ), + migrations.AddField( + model_name='subscription', + name='interval_count', + field=models.IntegerField(default=1), + ), + ] diff --git a/manage_subscriptions/models.py b/manage_subscriptions/models.py index b353ead..6201e02 100644 --- a/manage_subscriptions/models.py +++ b/manage_subscriptions/models.py @@ -35,8 +35,20 @@ class StripeProduct(BaseModel): class Subscription(BaseModel): + MONTH = "month" + DAY = "day" + WEEK = "week" + YEAR = "year" + + INTERVAL_TYPES = [ + (MONTH, "month"), + (DAY, "day"), + (WEEK, "week"), + (YEAR, "year"), + ] title = models.CharField(max_length=255) price_id = models.CharField(max_length=255, blank=True, null=True) + product_id = models.CharField(max_length=255, blank=True, null=True) stripe_product = models.ForeignKey( StripeProduct, related_name="subscription_product", @@ -50,6 +62,8 @@ class Subscription(BaseModel): plan = models.ForeignKey( Plan, related_name="subscription_plan", on_delete=models.CASCADE ) + interval = models.CharField(max_length=10, choices=INTERVAL_TYPES) + interval_count = models.IntegerField(default=1) high_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00) amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00) principal_types = models.ManyToManyField( @@ -69,25 +83,67 @@ class Subscription(BaseModel): def clean(self): # Ensure amount is greater than 1 - if self.amount <= 1: - raise ValidationError({"amount": "Amount must be greater than 1."}) + if not self.delete: + 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."} - # ) + # Ensure high_amount is greater than amount + if self.high_amount <= self.amount: + raise ValidationError( + {"high_amount": "High amount must be greater than amount."} + ) def save(self, *args, **kwargs): - self.clean() # Call clean before saving to ensure validation + from goodtimes.services import StripeService + self.clean() + if not self.is_free: + if self.price_id: + # Stipe dont provide to update the price record except active and deactive + price = StripeService.retrieve_price(self.price_id) + if not price["success"]: + raise Exception(price['message']) + + if self.active != price["data"].active: + StripeService.update_price(price_id=self.price_id, active=self.active) + else: + # Create new product and price + price = StripeService.create_price( + product_data={ + "name": self.title, + "description": self.short_description, + }, + unit_amount=int(self.amount * 100), + currency="gbp", + recurring={ + "interval": self.plan.title, + "interval_count": self.interval_count, + }, + metadata={ + "subscription_id": self.id + } + + ) + if not price["success"]: + raise Exception(price['message']) + + # add the id in record + self.price_id = price["data"].id + self.product_id = price["data"].product + super().save(*args, **kwargs) + def calculate_date(self): + count = { + self.DAY: 1, + self.MONTH: 30, # assuming a month is 30 days + self.YEAR: 365, + self.WEEK: 7 + } + + return count[self.interval] * self.interval_count + + + class SubscriptionStatus(models.TextChoices): ACTIVE = "active", _("Active") @@ -139,7 +195,18 @@ class PrincipalSubscription(BaseModel): @classmethod def has_principal_subscription(cls, principal): - return cls.get_active_princial_subscription(principal).exists() + return cls.get_grace_period_princial_subscription(principal).exists() + + @classmethod + def get_grace_period_princial_subscription(cls, principal): + return cls.objects.filter( + principal=principal, + is_paid=True, + # cancelled=False, + active=True, + # status=SubscriptionStatus.ACTIVE, + grace_period_end_date__gt=timezone.now().date(), + ) @classmethod def get_active_princial_subscription(cls, principal): @@ -147,24 +214,28 @@ class PrincipalSubscription(BaseModel): principal=principal, is_paid=True, # cancelled=False, - deleted=False, active=True, # status=SubscriptionStatus.ACTIVE, - grace_period_end_date__gt=timezone.now().date(), + end_date__gt=timezone.now().date(), ) - @classmethod def get_principal_subscription(cls, principal): return cls.objects.filter( principal=principal, is_paid=True, # cancelled=False, - deleted=False, active=True, # status=SubscriptionStatus.ACTIVE, ).order_by("-grace_period_end_date").first() + @classmethod + def cancel_stipe_auto_renew_subscription(cls, subscription): + subscription.status = SubscriptionStatus.INACTIVE + subscription.auto_renew = False + subscription.cancelled_date_time = timezone.now() + subscription.save() + class WebhookEvent(BaseModel): event_id = models.CharField(max_length=255, unique=True, db_index=True) diff --git a/manage_subscriptions/urls.py b/manage_subscriptions/urls.py index e26620d..bc69c93 100644 --- a/manage_subscriptions/urls.py +++ b/manage_subscriptions/urls.py @@ -13,11 +13,6 @@ urlpatterns = [ name="subscription_add", ), path("subscription//", views.SubscriptionDetailView.as_view(), name="subscription_detail"), - # path( - # "subscription/edit//", - # views.SubscriptionCreateOrUpdateView.as_view(), - # name="subscription_edit", - # ), path( "subscription/delete/", views.SubscriptionDeleteView.as_view(), @@ -32,28 +27,9 @@ urlpatterns = [ views.StripeProductCreateOrUpdateView.as_view(), name="stripe_product_add", ), - # path( - # "product/delete/", - # views.StripeProductDeleteView.as_view(), - # name="stripe_product_delete", - # ), # PLANS path("plan/list/", views.PlanView.as_view(), name="plan_list"), - # path( - # "plan/add/", - # views.PlanCreateOrUpdateView.as_view(), - # name="plan_add", - # ), - # path( - # "plan/edit//", - # views.PlanCreateOrUpdateView.as_view(), - # name="plan_edit", - # ), - # path( - # "plan/delete/", - # views.PlanDeleteView.as_view(), - # name="plan_delete", - # ), + # Principal Subscription path( "principal_subscription/list/", @@ -97,7 +73,8 @@ urlpatterns = [ ), path("stripe/", views.SubscriptionPageView.as_view(), name="stripe"), path("active/", views.ActiveSubscriptionView.as_view(), name="active"), - path("cancel-subscription/", views.CancelSubscriptionView.as_view(), name="cancel_subscription"), + path("cancel-subscription/", views.CancelAutoSubscriptionView.as_view(), name="cancel_subscription"), + path("404/", views.ErrorView.as_view(), name="error"), path("success/", views.SuccessView.as_view(), name="success"), path("cancel/", views.CancelView.as_view(), name="cancel"), path("subscription-cancel-success/", views.SubscriptionCancelSuccessView.as_view(), name="subscription_cancel_success"), diff --git a/manage_subscriptions/views.py b/manage_subscriptions/views.py index ee5cd54..13a3c31 100644 --- a/manage_subscriptions/views.py +++ b/manage_subscriptions/views.py @@ -9,6 +9,7 @@ from django.contrib.auth import login import jwt from django.utils import timezone from django.contrib.auth import get_user_model +from goodtimes.services import StripeService from manage_coupons.models import Coupon from manage_subscriptions.forms import ( StripeProductForm, @@ -112,57 +113,10 @@ class SubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): context = self.get_context_data(form=form) return render(request, self.template_name, context=context) - # Processing Stripe price creation and handling free subscription - success, message = self.handle_stripe_price(form) - if not success: - messages.error(self.request, message) - context = self.get_context_data(form=form) - return render(request, self.template_name, context=context) - form.save() messages.success(self.request, self.get_success_message()) return redirect(self.success_url) - def handle_stripe_price(self, form): - try: - stripe.api_key = settings.STRIPE_SECRET_KEY - stripe_product_id = ( - form.instance.stripe_product.product_id - if form.instance.stripe_product - else None - ) - # creating Stripe price only if the subscription is not free - if not form.cleaned_data.get("is_free") and stripe_product_id: - # Getting Stripe Product ID - stripe_product = form.instance.stripe_product - plan = form.instance.plan - - # Map the Plan interval to Stripe's recurring interval - # It will only work if the plan title is 'month', 'year' 'week' or 'day' - stripe_interval = plan.title - # Create the Stripe price - stripe_price = stripe.Price.create( - unit_amount=int( - form.cleaned_data["amount"] * 100 - ), # Amount in cents - currency="gbp", # Adjust the currency as needed - recurring={ - "interval": stripe_interval - }, # Use the interval from Plan - product=stripe_product.product_id, - ) - # Assign the Stripe price ID to the subscription - form.instance.price_id = stripe_price.id - else: - form.instance.price_id = None # No price ID for free subscriptions - - return True, "" # Success - except stripe.error.StripeError as e: - return False, f"Stripe error: {str(e)}" - except Exception as e: - return False, f"An error occurred: {str(e)}" - - class SubscriptionView(LoginRequiredMixin, generic.ListView): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS @@ -213,21 +167,6 @@ class SubscriptionDeleteView(LoginRequiredMixin, generic.View): try: # Retrieve the subscription object subscription = self.model.objects.get(id=pk) - - # Checking if there is a Stripe Price ID associated with the subscription - stripe_price_id = subscription.price_id - if stripe_price_id: - stripe.api_key = settings.STRIPE_SECRET_KEY - - try: - # Updating the Stripe price to mark it as inactive - stripe.Price.modify(stripe_price_id, active=False) - except stripe.error.StripeError as e: - # Handle Stripe errors - messages.error(request, f"Stripe error: {str(e)}") - return redirect(self.success_url) - - # Updating the subscription model record subscription.deleted = True subscription.active = False subscription.save() @@ -346,126 +285,6 @@ class StripeProductView(LoginRequiredMixin, generic.ListView): return context -""" we are not using product delete functionality because there may be multiple stripe's prices - attached to one product and in case of any error it will mismatch the stripe's price with - our database Subscription objects""" -# class StripeProductDeleteView(LoginRequiredMixin, generic.View): -# page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS -# resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS -# action = resource_action.ACTION_DELETE -# model = StripeProduct -# success_url = reverse_lazy("manage_subscriptions:stripe_product_list") -# success_message = constants.RECORD_DELETED -# error_message = constants.RECORD_NOT_FOUND - -# def get(self, request, pk): -# try: -# # Retrieve the subscription object -# product = self.model.objects.get(id=pk) - -# # Fetching the related subscriptions (prices) -# related_subscriptions = Subscription.objects.filter(stripe_product=product) - -# # Checking if there is a Stripe Product ID associated with the subscription -# stripe_product_id = product.product_id -# if stripe_product_id: -# stripe.api_key = settings.STRIPE_SECRET_KEY - -# # Deactivating related prices on Stripe first -# for subscription in related_subscriptions: -# price_id = subscription.price_id -# if price_id: -# try: -# stripe.Price.modify(price_id, active=False) -# except stripe.error.StripeError as e: -# # Handle Stripe errors -# messages.error(request, f"Stripe error: {str(e)}") -# return redirect(self.success_url) - -# try: -# # Updating the Stripe price to mark it as inactive -# stripe.Product.modify(stripe_product_id, active=False) -# except stripe.error.StripeError as e: -# # Handle Stripe errors -# messages.error(request, f"Stripe error: {str(e)}") -# return redirect(self.success_url) - -# # Updating the subscription model record -# product.deleted = True -# product.active = False -# product.save() - -# messages.success(request, self.success_message) - -# except self.model.DoesNotExist: -# messages.error(request, self.error_message) - -# return redirect(self.success_url) - - -# class PlanCreateOrUpdateView(LoginRequiredMixin, generic.View): -# # Set the page_name and resource -# page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS -# resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS - -# # Initialize the action as ACTION_CREATE (can change based on logic) -# action = resource_action.ACTION_CREATE # Default action - -# template_name = "manage_subscriptions/plan_add.html" -# model = Plan -# form_class = PlanForm -# success_url = reverse_lazy("manage_subscriptions:plan_list") -# error_message = "An error occurred while saving the data." - -# # Determine the success message dynamically based on whether it's an update or create -# def get_success_message(self): -# self.success_message = ( -# constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED -# ) -# return self.success_message - -# # Get the object (if exists) based on URL parameter 'pk' -# def get_object(self): -# pk = self.kwargs.get("pk") -# return get_object_or_404(self.model, pk=pk) if pk else None - -# # Add page_name and operation to the context -# def get_context_data(self, **kwargs): -# context = { -# "page_name": self.page_name, -# "operation": "Add" if not self.object else "Edit", -# } -# context.update(kwargs) # Include any additional context data passed to the view -# return context - -# def get(self, request, *args, **kwargs): -# self.object = self.get_object() - -# # If an object is found, change action to ACTION_UPDATE -# if self.object is not None: -# self.action = resource_action.ACTION_UPDATE - -# form = self.form_class(instance=self.object) -# context = self.get_context_data(form=form) -# return render(request, self.template_name, context=context) - -# def post(self, request, *args, **kwargs): -# self.object = self.get_object() - -# # If an object is found, change action to ACTION_UPDATE -# if self.object is not None: -# self.action = resource_action.ACTION_UPDATE - -# form = self.form_class(request.POST, instance=self.object) -# if not form.is_valid(): -# print(form.errors) -# context = self.get_context_data(form=form) -# return render(request, self.template_name, context=context) -# form.save() -# messages.success(self.request, self.get_success_message()) -# return redirect(self.success_url) - - class PlanView(LoginRequiredMixin, generic.ListView): page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS @@ -483,28 +302,6 @@ class PlanView(LoginRequiredMixin, generic.ListView): return context -# class PlanDeleteView(LoginRequiredMixin, generic.View): -# page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS -# resource = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS -# action = resource_action.ACTION_DELETE -# model = Plan -# success_url = reverse_lazy("manage_subscriptions:plan_list") -# success_message = constants.RECORD_DELETED -# error_message = constants.RECORD_NOT_FOUND - -# def get(self, request, pk): -# try: -# type_obj = self.model.objects.get(id=pk) -# type_obj.deleted = True -# type_obj.active = False -# type_obj.save() -# messages.success(request, self.success_message) -# except self.model.DoesNotExist: -# messages.success(request, self.error_message) - -# return redirect(self.success_url) - - class PrincipalSubscriptionCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource page_name = resource_action.RESOURCE_MANAGE_SUBSCRIPTIONS @@ -621,117 +418,135 @@ class PrincipalSubscriptionDeleteView(LoginRequiredMixin, generic.View): return redirect(self.success_url) -class SubscriptionPageView(TemplateView): +class SubscriptionPageView(generic.View): template_name = "stripe_html/index.html" + model = Subscription + error_url = reverse_lazy("manage_subscriptions:error") - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - request = self.request - if request.user.is_authenticated: - print("request.user: ", request.user) - subscriptions = Subscription.objects.filter( - principal_types=request.user.principal_type, - active=True, - deleted=False, - is_free=False, - ) + def get(self, request): + if not request.user.is_authenticated: + return HttpResponseRedirect(self.error_url) - if subscriptions.exists(): - context["subscriptions"] = subscriptions - context["stripeCheckoutUrl"] = settings.STRIPE_CHECKOUT_URL - context["stripeFinalUrl"] = settings.STRIPE_FINAL_URL - context["couponValidityCheckUrl"] = settings.COUPON_VALIDITY_CHECK_URL - else: - # Handling the case where no subscriptions are found for the principal type. - context["error"] = "No subscriptions found for your user type." - return context + print("request user is :", request.user) + obj = self.model.objects.filter( + principal_types=request.user.principal_type, + active=True, + is_free=False, + ) + if not obj.exists(): + print(f"No pre-define subscription details found in {self.model} table for user_type {request.user.principal_type}") + return HttpResponseRedirect(self.error_url) -class ActiveSubscriptionView(TemplateView): + context = { + "subscriptions": obj, + # "stripeCheckoutUrl": request.build_absolute_uri(reverse("manage_subscriptions:create_checkout_session")), + # "couponValidityCheckUrl": request.build_absolute_uri(reverse("manage_subscriptions:validate_coupon")), + "stripeCheckoutUrl": settings.STRIPE_CHECKOUT_URL, + "couponValidityCheckUrl": settings.COUPON_VALIDITY_CHECK_URL, + "stripe_public_key": settings.STRIPE_PUBLISH_KEY + } + return render(request, self.template_name, context=context) + +class ActiveSubscriptionView(generic.View): template_name = "stripe_html/active_subscription.html" + model = IAmPrincipal def get(self, request, *args, **kwargs): - token = request.GET.get("token") or request.session.get("jwt") + token = request.GET.get("token") print("token: ", token) + if token: - request.session["jwt"] = token - print("request.session: ", request.session) try: # Decode and validate token payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) - print("payload: ", payload) - user = get_user_model().objects.get(id=payload["user_id"]) + user = self.model.objects.get(id=payload["user_id"]) # Manually specify the authentication backend user.backend = "django.contrib.auth.backends.ModelBackend" # Log the user in login(request, user) - print("Logged in user: ", user) except ( IAmPrincipal.DoesNotExist, jwt.ExpiredSignatureError, jwt.InvalidTokenError, ): - return HttpResponseBadRequest("Invalid token or user not found") + return HttpResponseRedirect(reverse("manage_subscriptions:error")) + today = timezone.now().date() if request.user.is_authenticated: latest_subscription = PrincipalSubscription.objects.filter( principal=request.user, is_paid=True, - deleted=False, end_date__gte=today, ).order_by('-end_date').last() if not latest_subscription: return HttpResponseRedirect(reverse("manage_subscriptions:stripe")) - return super().get(request, *args, **kwargs) + + return render(request, self.template_name, context={"subscription": latest_subscription}) + return HttpResponseRedirect(reverse("manage_subscriptions:error")) - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - request = self.request - today = timezone.now().date() - if request.user.is_authenticated: - latest_subscription = PrincipalSubscription.objects.filter( - principal=request.user, - is_paid=True, - deleted=False, - end_date__gte=today, - ).order_by('-end_date').last() - context["active_subscription"] = latest_subscription - return context +class CancelAutoSubscriptionView(LoginRequiredMixin, generic.View): + model = PrincipalSubscription + error_url = reverse_lazy("manage_subscriptions:error") - -class CancelSubscriptionView(LoginRequiredMixin, generic.View): - def post(self, request, *args, **kwargs): - subscription_id = request.POST.get("subscription_id") + def get(self, request, *args, **kwargs): + subscription_id = self.kwargs.get("subscription_id") try: - subscription = PrincipalSubscription.objects.get( + subscription = self.model.objects.get( id=subscription_id, principal=request.user ) - except PrincipalSubscription.DoesNotExist: + except self.model.DoesNotExist: messages.error(request, "Subscription not found.") - return redirect("manage_subscriptions:cancel") + return redirect("manage_subscriptions:error") try: - with transaction.atomic(): - if subscription.is_stripe_subscription: - # Cancel Stripe subscription - stripe.Subscription.modify( - subscription.stripe_subscription_id, cancel_at_period_end=True - ) + if subscription.stripe_subscription_id: + data = StripeService.cancel_auto_renew_subscription(subscription.stripe_subscription_id) + if not data["success"]: + return redirect(self.error_url) - # Updating subscription status in the local database - subscription.status = SubscriptionStatus.INACTIVE - subscription.cancelled = True - subscription.auto_renew = False - subscription.cancelled_date_time = timezone.now() - subscription.save() + self.model.cancel_stipe_auto_renew_subscription(subscription) - messages.success(request, "Subscription cancelled successfully.") - return redirect("manage_subscriptions:subscription_cancel_success") - except stripe.error.InvalidRequestError as e: - messages.error(request, f"Stripe error: {str(e)}") - return redirect("manage_subscriptions:subscription_cancel_fails") + except Exception as e: + print(f'an error occur {str(e)}') + messages.error(request, f"An error occurred while cancelling the subscription {str(e)}") + return redirect(self.error_url) + + return redirect(reverse_lazy("manage_subscriptions:active")) + + # def post(self, request, *args, **kwargs): + # subscription_id = request.POST.get("subscription_id") + + # try: + # subscription = PrincipalSubscription.objects.get( + # id=subscription_id, principal=request.user + # ) + # except PrincipalSubscription.DoesNotExist: + # messages.error(request, "Subscription not found.") + # return redirect("manage_subscriptions:cancel") + + # try: + # with transaction.atomic(): + # if subscription.is_stripe_subscription: + # # Cancel Stripe subscription + # stripe.Subscription.modify( + # subscription.stripe_subscription_id, cancel_at_period_end=True + # ) + + # # Updating subscription status in the local database + # subscription.status = SubscriptionStatus.INACTIVE + # subscription.cancelled = True + # subscription.auto_renew = False + # subscription.cancelled_date_time = timezone.now() + # subscription.save() + + # messages.success(request, "Subscription cancelled successfully.") + # return redirect("manage_subscriptions:subscription_cancel_success") + # except stripe.error.InvalidRequestError as e: + # messages.error(request, f"Stripe error: {str(e)}") + # return redirect("manage_subscriptions:subscription_cancel_fails") @csrf_exempt @@ -826,10 +641,12 @@ def create_checkout_session(request): data = json.loads(request.body) subscription_id = data.get("subscriptionId") coupon_code = data.get("couponCode") - transaction_amount = data.get("discountAmount") + transaction_amount = data.get("finalAmount") is_recurring = data.get("isRecurring") principal_id = request.user.id + print(f"subscription data is {subscription_id}, {coupon_code}, { is_recurring}") + try: subscription = Subscription.objects.get(id=subscription_id) except Subscription.DoesNotExist: @@ -842,14 +659,10 @@ def create_checkout_session(request): "success_url": request.build_absolute_uri("/subscriptions/success/"), "cancel_url": request.build_absolute_uri("/subscriptions/cancel/"), "metadata": { - "transaction_amount": str(transaction_amount), + "transaction_amount": str(subscription.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 - ), + "product_id": subscription.product_id, "couponCode": coupon_code if coupon_code else None, }, } @@ -900,6 +713,8 @@ def create_checkout_session(request): return JsonResponse({"error": str(e)}, status=500) +class ErrorView(TemplateView): + template_name = "stripe_html/webview_404.html" class SuccessView(TemplateView): template_name = "stripe_html/success.html" diff --git a/templates/manage_subscriptions/subscription_list.html b/templates/manage_subscriptions/subscription_list.html index 41afe54..77c1843 100644 --- a/templates/manage_subscriptions/subscription_list.html +++ b/templates/manage_subscriptions/subscription_list.html @@ -48,7 +48,10 @@ style="width: 69.2656px;"> Title Plan Days + style="width: 69.2656px;"> Interval + Interval Count Amount @@ -58,9 +61,6 @@ Free for Admin - Stripe Product Active {{data_obj.id}} {{data_obj.title}} - {{data_obj.plan.days}} + {{data_obj.interval | capfirst}} + {{data_obj.interval_count }} {{data_obj.amount}} {% if data_obj.principal_types.all %} @@ -88,13 +89,6 @@ {{data_obj.is_free}} - - {% if data_obj.stripe_product %} - {{ data_obj.stripe_product.product_id }} - {% else %} - N/A - {% endif %} - {{data_obj.active}} diff --git a/templates/stripe_html/active_subscription.html b/templates/stripe_html/active_subscription.html index c6bef02..5970cf9 100644 --- a/templates/stripe_html/active_subscription.html +++ b/templates/stripe_html/active_subscription.html @@ -16,14 +16,13 @@ } .container { - padding: 40px 15px; + padding: 0 15px 20px; } .card { background-color: var(--light-black); border: 1px solid var(--main-yellow); border-radius: 8px; - margin-top: 20px; padding: 20px; } @@ -93,7 +92,7 @@ border-bottom: 2px solid #d4af37 !important; } .btn-outline-gold { - color: #d4af37; + color: #000; border-color: #d4af37; } .btn-outline-gold:hover { @@ -107,44 +106,40 @@
-

Your Active Subscription

+

Subscription Details

-

{{ active_subscription.subscription.title }}

+

{{ subscription.subscription.title }}

Full Name:
-

{{ active_subscription.principal.first_name }} {{ active_subscription.principal.last_name }}

-

Status: {{ active_subscription.get_status_display }}

-

Start Date: {{ active_subscription.start_date }}

-

End Date: {{ active_subscription.end_date }}

-

Auto Renew: {{ active_subscription.auto_renew|yesno:"Yes,No" }}

+

{{ subscription.principal.first_name }} {{ subscription.principal.last_name }}

+

Status: {{ subscription.get_status_display }}

+

Start Date: {{ subscription.start_date }}

+

End Date: {{ subscription.end_date }}

+

Auto Renew: {{ subscription.auto_renew|yesno:"Yes,No" }}

- {% if active_subscription.coupon_code %} -

Coupon Code: {{ active_subscription.coupon_code }}

+ {% if subscription.coupon_code %} +

Coupon Code: {{ subscription.coupon_code }}

{% endif %} - {% if active_subscription.cancelled %} + {% if subscription.cancelled_date_time %}
-

Cancellation Details

-

Cancelled: Yes

-

Cancellation Date: {{ active_subscription.cancelled_date_time }}

-

Grace Period Ends: {{ active_subscription.grace_period_end_date }}

+

Auto renew cancellation details

+

Cancellation Date: {{ subscription.cancelled_date_time }}

+

Grace Period End Date: {{ subscription.grace_period_end_date }}

{% endif %} - {% if active_subscription.auto_renew and not active_subscription.cancelled %} -
-

Cancel Subscription

-
- {% csrf_token %} - - -
-
+ {% if subscription.auto_renew and not subscription.cancelled_date_time %} +
+

Cancel Auto-Renewing Subscription

+

Click the button below to cancel your auto-renewing subscription. This will prevent future payments from being processed.

+ Cancel Auto-Renewal +
{% endif %}
diff --git a/templates/stripe_html/index.html b/templates/stripe_html/index.html index d811599..61bc410 100644 --- a/templates/stripe_html/index.html +++ b/templates/stripe_html/index.html @@ -120,34 +120,25 @@ {% else %}

£ {{ subscription.amount }}

{% endif %} - {% if subscription.plan.days %} -

Days of Subscription: {{ subscription.plan.days }}

- {% else %} -

Days of Subscription: Not available

- {% endif %} +

Subscription Cycle: {{subscription.interval_count}} {{ subscription.interval | capfirst }}

- + {% comment %} {% endcomment %}
- -
- +
- - - - {% empty %} -

No subscriptions available.

{% endfor %} @@ -523,38 +514,82 @@ diff --git a/templates/stripe_html/webview_404.html b/templates/stripe_html/webview_404.html new file mode 100644 index 0000000..803c753 --- /dev/null +++ b/templates/stripe_html/webview_404.html @@ -0,0 +1,129 @@ + + + + + + + Active Subscription + + + + + + + {% comment %}
+

Your Active Subscription

+
{% endcomment %} + +
+
+
+

404

+
+
+
An error occurred. Please try again later.
+
+
+
+ + + + + + +