refactor(subscription): removed unnecessary complexity
This commit is contained in:
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.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)
|
||||
# )
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user