refactor(subscription): removed unnecessary complexity

This commit is contained in:
bobbyvish
2024-08-21 23:24:47 +05:30
parent 4866f0a5d4
commit 5d107ad17a
8 changed files with 207 additions and 47 deletions

View File

@@ -1051,10 +1051,111 @@ class StripeService:
# stipe not provide to delete the price
@staticmethod
def cancel_auto_renew_subscription(subscription_id: str) -> dict:
def create_coupon(
amount_off: int = None,
percent_off: float = None,
duration: str = "once",
name: str = None,
currency: str = None,
redeem_by: datetime = None,
max_redemptions: int = 0,
metadata: dict = None
) -> dict:
"""
Creates a Stripe Coupon with either a fixed amount off or a percentage off.
:param amount_off: The discount amount to be applied (in the smallest currency unit, e.g., cents). This cannot be used in conjunction with `percent_off`.
:param percent_off: The discount percentage to be applied to the price. This cannot be used in conjunction with `amount_off`.
:param duration: The duration for which the coupon is valid. Valid values are:
- "once": The coupon will apply to the next invoice only.
:param name: An optional name for the coupon.
:param currency: The currency in which the `amount_off` is specified. Required if `amount_off` is used.
:param redeem_by: A timestamp at which the coupon will no longer be redeemable.
The coupon can still be applied to invoices created after the `redeem_by` date,
if the subscription was active prior to the date.
:param max_redemptions: The maximum number of times this coupon can be redeemed in total.
Defaults to 0, meaning unlimited redemptions.
:param metadata: A set of key-value pairs to store additional information about the coupon in Stripe.
:return: A dictionary containing:
- 'success': Boolean indicating the success of the operation.
- 'data': The created Stripe Coupon object if successful.
- 'message': Error message if the operation failed.
:raises ValueError: If both `amount_off` and `percent_off` are provided, or if neither is provided.
Also raised if `amount_off` is provided without a corresponding `currency`.
:raises stripe.error.StripeError: If an error occurs while creating the coupon via the Stripe API.
See: https://docs.stripe.com/api/coupons/create?lang=python
"""
if amount_off and percent_off:
raise ValueError("You can provide either `amount_off` or `percent_off`, but not both.")
if not amount_off and not percent_off:
raise ValueError("You must provide either `amount_off` or `percent_off`.")
if amount_off and not currency:
raise ValueError("Currency must be provided when `amount_off` is specified.")
coupon_data = {
"duration": duration,
"name": name,
"redeem_by": redeem_by,
"max_redemptions": max_redemptions,
"metadata": metadata,
}
if amount_off:
coupon_data.update({
"amount_off": amount_off,
"currency": currency,
})
elif percent_off:
coupon_data.update({
"percent_off": percent_off,
})
try:
coupon = stripe.Coupon.create(**coupon_data)
return {'success': True, 'data': coupon}
except stripe.error.StripeError as e:
return {'success': False, 'message': f"Error creating coupon: {e}"}
@staticmethod
def retrieve_coupon(coupon_id: str):
"""
Retrieve a Stripe Coupon by its ID.
:param coupon_id: The ID of the coupon to retrieve.
:return: The retrieved Stripe Coupon object.
See: https://docs.stripe.com/api/coupons/retrieve?lang=python
"""
try:
coupon = stripe.Coupon.retrieve(coupon_id)
return {'success': True, 'data': coupon}
except stripe.error.StripeError as e:
return {'success': False, 'message': f"Error retrieving coupon: {e}"}
@staticmethod
def delete_coupon(coupon_id: str):
"""
Retrieve a Stripe Coupon by its ID.
:param coupon_id: The ID of the coupon to retrieve.
:return: The retrieved Stripe Coupon object.
"""
try:
coupon = stripe.Coupon.delete(coupon_id)
return {'success': True, 'data': coupon}
except stripe.error.StripeError as e:
return {'success': False, 'message': f"Error deleting coupon: {e}"}
@staticmethod
def cancel_auto_renew_subscription(subscription_id: str):
"""
Cancels the auto-renewal of a Stripe subscription.
:param subscription_id: The ID of the subscription to cancel auto-renewal for.
:return: A dictionary with success status and the updated subscription object or an error message.
"""

View File

@@ -1,7 +1,7 @@
from datetime import timedelta
from django.utils import timezone
import datetime
from manage_subscriptions.models import PrincipalSubscription
from manage_subscriptions.models import PrincipalSubscription, SubscriptionStatus
class SubscriptionService:
@@ -33,6 +33,8 @@ class SubscriptionService:
current_period_start, current_period_end, subscription.calulate_days()
)
PrincipalSubscription.objects.filter(principal=principal, status=SubscriptionStatus.ACTIVE).update(status=SubscriptionStatus.EXPIRED)
principal_subscription = PrincipalSubscription.objects.create(
principal=principal,
subscription=subscription,

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.0.2 on 2024-08-21 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_coupons', '0002_coupon_coupon_id'),
]
operations = [
migrations.AlterField(
model_name='coupon',
name='discount_amount',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Representing the amount to subtract from an invoice total (required if discount_percentage is not passed)', max_digits=10, null=True),
),
migrations.AlterField(
model_name='coupon',
name='discount_percentage',
field=models.DecimalField(blank=True, decimal_places=2, help_text='A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if discount_amount is not passed).', max_digits=5, null=True),
),
migrations.AlterField(
model_name='coupon',
name='max_redeems',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='coupon',
name='valid_to',
field=models.DateTimeField(help_text='Datetime for the last redeemable date. After this, the coupon is invalid for new customers.'),
),
]

View File

@@ -1,3 +1,4 @@
from decimal import Decimal
from django.db import models
from django.utils import timezone
from accounts.models import BaseModel, IAmPrincipalType
@@ -12,18 +13,21 @@ class Coupon(BaseModel):
description = models.TextField(null=True, blank=True)
image = models.ImageField(upload_to="coupon_img", null=True, blank=True)
discount_amount = models.DecimalField(
max_digits=10, decimal_places=2, null=True, blank=True
max_digits=10, decimal_places=2, null=True, blank=True, help_text="Representing the amount to subtract from an invoice total (required if discount_percentage is not passed)"
)
discount_percentage = models.DecimalField(
max_digits=5, decimal_places=2, null=True, blank=True
max_digits=5, decimal_places=2, null=True, blank=True, help_text="A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if discount_amount is not passed)."
)
valid_from = models.DateTimeField()
valid_to = models.DateTimeField()
max_redeems = models.IntegerField(default=0)
valid_to = models.DateTimeField(help_text="Datetime for the last redeemable date. After this, the coupon is invalid for new customers.")
max_redeems = models.IntegerField(default=1)
class Meta:
db_table = "coupon"
def __str__(self):
return self.coupon_code
def clean(self):
"""
Validate the Coupon instance. Ensure that the `max_redeems` is greater than 0,
@@ -61,19 +65,40 @@ class Coupon(BaseModel):
)
def save(self, *args, **kwargs):
self.clean() # Call clean before saving to ensure validation
super().save(*args, **kwargs)
from goodtimes.services import StripeService
if not self.delete:
self.clean() # Call clean before saving to ensure validation
def __str__(self):
return self.coupon_code
if not self.pk and not self.coupon_id:
amount_off = int(self.discount_amount * Decimal(100)) if self.discount_amount else None
percent_off = float(self.discount_percentage) if self.discount_percentage else None
result = StripeService.create_coupon(
amount_off=amount_off,
percent_off=percent_off,
duration="once",
name=self.title,
redeem_by=int(self.valid_to.timestamp()),
max_redemptions=self.max_redeems,
currency='gbp',
metadata={"local_id": self.id}
)
if not result["success"]:
raise ValueError(f"Failed to create Stripe coupon: {result['message']}")
self.coupon_code = result['data'].id
self.coupon_id = result["data"].id
super().save(*args, **kwargs)
# If max_redeems is 0, it means that we are allowing unlimited redeems
def is_valid(self):
now = timezone.now()
return (
self.active
and not self.deleted
and self.valid_from <= now <= self.valid_to
and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems)
)
# def is_valid(self):
# now = timezone.now()
# return (
# self.active
# and not self.deleted
# and self.valid_from <= now <= self.valid_to
# and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems)
# )

View File

@@ -91,17 +91,9 @@ class CouponCreateOrUpdateView(LoginRequiredMixin, generic.View):
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
success, message = handle_stripe_coupon(
form.instance, settings.STRIPE_SECRET_KEY
)
if success:
messages.success(self.request, message)
return redirect(self.success_url)
else:
messages.error(self.request, message)
return render(
request, self.template_name, context=self.get_context_data(form=form)
)
form.save()
messages.success(request, self.get_success_message)
return redirect(self.success_url)
class CouponDeleteView(LoginRequiredMixin, generic.View):

View File

@@ -49,12 +49,20 @@ class SubscriptionForm(forms.ModelForm):
class PrincipalSubscriptionForm(forms.ModelForm):
class Meta:
model = PrincipalSubscription
fields = "__all__" # Includes all fields from the model
fields = [
"subscription",
"principal",
"status",
"start_date",
"end_date",
"grace_period_end_date",
"comments",
"coupon_code"
] # Includes all fields from the model
widgets = {
"start_date": forms.DateInput(attrs={"type": "date"}),
"end_date": forms.DateInput(attrs={"type": "date"}),
"grace_period_end_date": forms.DateInput(attrs={"type": "date"}),
"cancelled_date_time": forms.DateTimeInput(attrs={"type": "datetime"}),
}

View File

@@ -83,19 +83,20 @@ class Subscription(BaseModel):
def clean(self):
# Ensure amount is greater than 1
if not self.delete:
if self.amount <= 1:
raise ValidationError({"amount": "Amount must be 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 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):
from goodtimes.services import StripeService
self.clean()
if not self.delete:
self.clean()
if not self.is_free:
if self.price_id:
# Stipe dont provide to update the price record except active and deactive
@@ -204,7 +205,7 @@ class PrincipalSubscription(BaseModel):
is_paid=True,
# cancelled=False,
active=True,
# status=SubscriptionStatus.ACTIVE,
status=SubscriptionStatus.ACTIVE,
grace_period_end_date__gt=timezone.now().date(),
)
@@ -215,7 +216,7 @@ class PrincipalSubscription(BaseModel):
is_paid=True,
# cancelled=False,
active=True,
# status=SubscriptionStatus.ACTIVE,
status=SubscriptionStatus.ACTIVE,
end_date__gt=timezone.now().date(),
)
@@ -226,12 +227,11 @@ class PrincipalSubscription(BaseModel):
is_paid=True,
# cancelled=False,
active=True,
# status=SubscriptionStatus.ACTIVE,
status=SubscriptionStatus.ACTIVE,
).order_by("-grace_period_end_date").first()
@classmethod
def cancel_stipe_auto_renew_subscription(cls, subscription):
subscription.status = SubscriptionStatus.INACTIVE
subscription.auto_renew = False
subscription.cancelled_date_time = timezone.now()
subscription.save()

View File

@@ -653,14 +653,13 @@ def create_checkout_session(request):
return JsonResponse({"error": "Subscription not found."}, status=404)
# Default transaction amount based on subscription amount
print("Before Session Data")
session_data = {
"payment_method_types": ["card"],
"success_url": request.build_absolute_uri("/subscriptions/success/"),
"cancel_url": request.build_absolute_uri("/subscriptions/cancel/"),
"metadata": {
"transaction_amount": str(subscription.amount),
"principal": str(request.user.id),
"principal": str(principal_id),
"subscription_id": str(subscription.id),
"product_id": subscription.product_id,
"couponCode": coupon_code if coupon_code else None,