refactor(subscription): removed unnecessary complexity
This commit is contained in:
@@ -1051,10 +1051,111 @@ class StripeService:
|
|||||||
# stipe not provide to delete the price
|
# stipe not provide to delete the price
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
Cancels the auto-renewal of a Stripe subscription.
|
||||||
|
|
||||||
:param subscription_id: The ID of the subscription to cancel auto-renewal for.
|
: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.
|
:return: A dictionary with success status and the updated subscription object or an error message.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import datetime
|
import datetime
|
||||||
from manage_subscriptions.models import PrincipalSubscription
|
from manage_subscriptions.models import PrincipalSubscription, SubscriptionStatus
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionService:
|
class SubscriptionService:
|
||||||
@@ -33,6 +33,8 @@ class SubscriptionService:
|
|||||||
current_period_start, current_period_end, subscription.calulate_days()
|
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_subscription = PrincipalSubscription.objects.create(
|
||||||
principal=principal,
|
principal=principal,
|
||||||
subscription=subscription,
|
subscription=subscription,
|
||||||
|
|||||||
@@ -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.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from decimal import Decimal
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from accounts.models import BaseModel, IAmPrincipalType
|
from accounts.models import BaseModel, IAmPrincipalType
|
||||||
@@ -12,18 +13,21 @@ class Coupon(BaseModel):
|
|||||||
description = models.TextField(null=True, blank=True)
|
description = models.TextField(null=True, blank=True)
|
||||||
image = models.ImageField(upload_to="coupon_img", null=True, blank=True)
|
image = models.ImageField(upload_to="coupon_img", null=True, blank=True)
|
||||||
discount_amount = models.DecimalField(
|
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(
|
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_from = models.DateTimeField()
|
||||||
valid_to = models.DateTimeField()
|
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=0)
|
max_redeems = models.IntegerField(default=1)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "coupon"
|
db_table = "coupon"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.coupon_code
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
Validate the Coupon instance. Ensure that the `max_redeems` is greater than 0,
|
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):
|
def save(self, *args, **kwargs):
|
||||||
self.clean() # Call clean before saving to ensure validation
|
from goodtimes.services import StripeService
|
||||||
super().save(*args, **kwargs)
|
if not self.delete:
|
||||||
|
self.clean() # Call clean before saving to ensure validation
|
||||||
|
|
||||||
def __str__(self):
|
if not self.pk and not self.coupon_id:
|
||||||
return self.coupon_code
|
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
|
# If max_redeems is 0, it means that we are allowing unlimited redeems
|
||||||
|
|
||||||
def is_valid(self):
|
# def is_valid(self):
|
||||||
now = timezone.now()
|
# now = timezone.now()
|
||||||
return (
|
# return (
|
||||||
self.active
|
# self.active
|
||||||
and not self.deleted
|
# and not self.deleted
|
||||||
and self.valid_from <= now <= self.valid_to
|
# and self.valid_from <= now <= self.valid_to
|
||||||
and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems)
|
# and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems)
|
||||||
)
|
# )
|
||||||
|
|||||||
@@ -91,17 +91,9 @@ class CouponCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
|||||||
context = self.get_context_data(form=form)
|
context = self.get_context_data(form=form)
|
||||||
return render(request, self.template_name, context=context)
|
return render(request, self.template_name, context=context)
|
||||||
|
|
||||||
success, message = handle_stripe_coupon(
|
form.save()
|
||||||
form.instance, settings.STRIPE_SECRET_KEY
|
messages.success(request, self.get_success_message)
|
||||||
)
|
return redirect(self.success_url)
|
||||||
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):
|
class CouponDeleteView(LoginRequiredMixin, generic.View):
|
||||||
|
|||||||
@@ -49,12 +49,20 @@ class SubscriptionForm(forms.ModelForm):
|
|||||||
class PrincipalSubscriptionForm(forms.ModelForm):
|
class PrincipalSubscriptionForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PrincipalSubscription
|
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 = {
|
widgets = {
|
||||||
"start_date": forms.DateInput(attrs={"type": "date"}),
|
"start_date": forms.DateInput(attrs={"type": "date"}),
|
||||||
"end_date": forms.DateInput(attrs={"type": "date"}),
|
"end_date": forms.DateInput(attrs={"type": "date"}),
|
||||||
"grace_period_end_date": forms.DateInput(attrs={"type": "date"}),
|
"grace_period_end_date": forms.DateInput(attrs={"type": "date"}),
|
||||||
"cancelled_date_time": forms.DateTimeInput(attrs={"type": "datetime"}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -83,19 +83,20 @@ class Subscription(BaseModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# Ensure amount is greater than 1
|
# Ensure amount is greater than 1
|
||||||
if not self.delete:
|
if self.amount <= 1:
|
||||||
if self.amount <= 1:
|
raise ValidationError({"amount": "Amount must be greater than 1."})
|
||||||
raise ValidationError({"amount": "Amount must be greater than 1."})
|
|
||||||
|
|
||||||
# Ensure high_amount is greater than amount
|
# Ensure high_amount is greater than amount
|
||||||
if self.high_amount <= self.amount:
|
if self.high_amount <= self.amount:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"high_amount": "High amount must be greater than amount."}
|
{"high_amount": "High amount must be greater than amount."}
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
from goodtimes.services import StripeService
|
from goodtimes.services import StripeService
|
||||||
self.clean()
|
if not self.delete:
|
||||||
|
self.clean()
|
||||||
|
|
||||||
if not self.is_free:
|
if not self.is_free:
|
||||||
if self.price_id:
|
if self.price_id:
|
||||||
# Stipe dont provide to update the price record except active and deactive
|
# Stipe dont provide to update the price record except active and deactive
|
||||||
@@ -204,7 +205,7 @@ class PrincipalSubscription(BaseModel):
|
|||||||
is_paid=True,
|
is_paid=True,
|
||||||
# cancelled=False,
|
# cancelled=False,
|
||||||
active=True,
|
active=True,
|
||||||
# status=SubscriptionStatus.ACTIVE,
|
status=SubscriptionStatus.ACTIVE,
|
||||||
grace_period_end_date__gt=timezone.now().date(),
|
grace_period_end_date__gt=timezone.now().date(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -215,7 +216,7 @@ class PrincipalSubscription(BaseModel):
|
|||||||
is_paid=True,
|
is_paid=True,
|
||||||
# cancelled=False,
|
# cancelled=False,
|
||||||
active=True,
|
active=True,
|
||||||
# status=SubscriptionStatus.ACTIVE,
|
status=SubscriptionStatus.ACTIVE,
|
||||||
end_date__gt=timezone.now().date(),
|
end_date__gt=timezone.now().date(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,12 +227,11 @@ class PrincipalSubscription(BaseModel):
|
|||||||
is_paid=True,
|
is_paid=True,
|
||||||
# cancelled=False,
|
# cancelled=False,
|
||||||
active=True,
|
active=True,
|
||||||
# status=SubscriptionStatus.ACTIVE,
|
status=SubscriptionStatus.ACTIVE,
|
||||||
).order_by("-grace_period_end_date").first()
|
).order_by("-grace_period_end_date").first()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cancel_stipe_auto_renew_subscription(cls, subscription):
|
def cancel_stipe_auto_renew_subscription(cls, subscription):
|
||||||
subscription.status = SubscriptionStatus.INACTIVE
|
|
||||||
subscription.auto_renew = False
|
subscription.auto_renew = False
|
||||||
subscription.cancelled_date_time = timezone.now()
|
subscription.cancelled_date_time = timezone.now()
|
||||||
subscription.save()
|
subscription.save()
|
||||||
|
|||||||
@@ -653,14 +653,13 @@ def create_checkout_session(request):
|
|||||||
return JsonResponse({"error": "Subscription not found."}, status=404)
|
return JsonResponse({"error": "Subscription not found."}, status=404)
|
||||||
|
|
||||||
# Default transaction amount based on subscription amount
|
# Default transaction amount based on subscription amount
|
||||||
print("Before Session Data")
|
|
||||||
session_data = {
|
session_data = {
|
||||||
"payment_method_types": ["card"],
|
"payment_method_types": ["card"],
|
||||||
"success_url": request.build_absolute_uri("/subscriptions/success/"),
|
"success_url": request.build_absolute_uri("/subscriptions/success/"),
|
||||||
"cancel_url": request.build_absolute_uri("/subscriptions/cancel/"),
|
"cancel_url": request.build_absolute_uri("/subscriptions/cancel/"),
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"transaction_amount": str(subscription.amount),
|
"transaction_amount": str(subscription.amount),
|
||||||
"principal": str(request.user.id),
|
"principal": str(principal_id),
|
||||||
"subscription_id": str(subscription.id),
|
"subscription_id": str(subscription.id),
|
||||||
"product_id": subscription.product_id,
|
"product_id": subscription.product_id,
|
||||||
"couponCode": coupon_code if coupon_code else None,
|
"couponCode": coupon_code if coupon_code else None,
|
||||||
|
|||||||
Reference in New Issue
Block a user