Skip to content

Commit

Permalink
Expiry date on membership vouchers
Browse files Browse the repository at this point in the history
Add an expiry date
check subscriptions and remove discounts that expire before next payment date
allow auto-applied membership vouchers
custom voucher messages
  • Loading branch information
rebkwok committed Nov 3, 2024
1 parent 488b964 commit 294986a
Show file tree
Hide file tree
Showing 23 changed files with 386 additions and 121 deletions.
1 change: 0 additions & 1 deletion accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,6 @@ def get_context_data(self, **kwargs):
def form_valid(self, form):
user = self.request.user
next_url = self.request.POST.get("next_url")

SignedDataPrivacy.objects.get_or_create(
user=user, version=form.data_privacy_policy.version
)
Expand Down
1 change: 1 addition & 0 deletions booking/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ def booking(request):
# hide online tutorials
"online_tutorials_exist": False,
"show_memberships": settings.SHOW_MEMBERSHIPS,
"membership_voucher_message": settings.MEMBERSHIP_VOUCHER_MESSAGE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'''
Find all subscriptions with discounts applied.
Check if discount voucher code expires before next payment date
Remove discount if necessary
'''
import logging
from datetime import timedelta
import stripe

from django.utils import timezone
from django.conf import settings
from django.core.mail import send_mail
from django.template.loader import get_template
from django.core.management.base import BaseCommand

from booking.models import UserMembership, StripeSubscriptionVoucher
from booking.views.membership_views import ensure_subscription_up_to_date
from activitylog.models import ActivityLog
from stripe_payments.utils import StripeConnector


logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = 'Remove expired discounts'

def handle(self, *args, **options):
client = StripeConnector()
active = UserMembership.objects.filter(subscription_status="active")
for user_membership in active:
stripe_subscription = client.get_subscription(user_membership.subscription_id)
if stripe_subscription.discounts:
discount = stripe_subscription.discounts[0]
voucher = StripeSubscriptionVoucher.objects.get(promo_code_id=discount.promotion_code)
if voucher.expires_before_next_payment_date():
client.remove_discount_from_subscription(stripe_subscription.id)
self.stdout.write(
f"Expired discount code {voucher.code} removed from subscription from user {user_membership.username}"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 5.1.1 on 2024-11-03 13:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("booking", "0104_event_members_only"),
]

operations = [
migrations.AlterModelOptions(
name="stripesubscriptionvoucher",
options={"ordering": ("-active", "-expiry_date", "-redeem_by")},
),
migrations.AddField(
model_name="stripesubscriptionvoucher",
name="expiry_date",
field=models.DateTimeField(
blank=True,
help_text="Date after which the code will be removed from any memberships that have currently applied it.",
null=True,
),
),
migrations.AlterField(
model_name="stripesubscriptionvoucher",
name="redeem_by",
field=models.DateTimeField(
blank=True,
help_text="Date after which users can no longer apply the code; note that once applied, it will apply for the voucher duration, even if that duration extends beyond the redeem by date. i.e. if a voucher applies for 2 months, and is redeemed on the redeem by date, it will still apply for the next 2 months' membership. If you want to override this behaviour, set an expiry date as well.",
null=True,
),
),
]
49 changes: 45 additions & 4 deletions booking/models/membership_models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

from datetime import datetime
from dateutil.relativedelta import relativedelta
import logging
import re

Expand Down Expand Up @@ -431,14 +432,31 @@ class StripeSubscriptionVoucher(models.Model):
duration = models.CharField(max_length=255, choices=(("once", "once"), ("forever", "forever"), ("repeating", "repeating")), default="once")
duration_in_months = models.PositiveIntegerField(null=True, blank=True)
max_redemptions = models.PositiveIntegerField(null=True, blank=True)
redeem_by = models.DateTimeField(null=True, blank=True)
redeem_by = models.DateTimeField(
null=True, blank=True,
help_text=(
"Date after which users can no longer apply the code; note that once applied, it will apply for the voucher duration, "
"even if that duration extends beyond the redeem by date. i.e. if a voucher applies for 2 months, and is redeemed on the "
"redeem by date, it will still apply for the next 2 months' membership. If you want to override this behaviour, set an "
"expiry date as well."
)
)
expiry_date = models.DateTimeField(
null=True, blank=True,
help_text=(
"Date after which the code will be removed from any memberships that have currently applied it."
)
)
active = models.BooleanField(default=True)
# Apppy to new subscriptions only; this is not a Stripe-handled restriction (Stripe has 'first_time_transaction' restriction on
# promo codes, but they are customer-based, not product-based)
new_memberships_only = models.BooleanField(
default=True, help_text="Valid for new memberships only"
)

class Meta:
ordering = ("-active", "-expiry_date", "-redeem_by")

def __str__(self) -> str:
return self.code

Expand All @@ -449,25 +467,31 @@ def expired(self):

@property
def description(self):
suffix = ""
if self.expiry_date:
suffix = f" (expires on {self.expiry_date.strftime('%d-%b-%Y')})"

if self.amount_off:
discount = f"£{self.amount_off} off"
else:
discount = f"{self.percent_off}% off"

match self.duration:
case "once":
return f"{discount} one month's membership"
return f"{discount} one month's membership{suffix}"
case "forever":
return discount
return f"{discount}{suffix}"
case "repeating":
return f"{discount} {self.duration_in_months} months membership"
return f"{discount} {self.duration_in_months} months membership{suffix}"

def save(self, *args, **kwargs):
presaved = None
if not self.id:
self.code = self.code.lower()
else:
presaved = StripeSubscriptionVoucher.objects.get(id=self.id)
if self.expiry_date:
self.expiry_date.replace(hour=23, minute=59)
super().save(*args, **kwargs)
stripe_client = StripeConnector()
if presaved:
Expand Down Expand Up @@ -496,3 +520,20 @@ def create_stripe_code(self):
)
self.promo_code_id = promo_code.id
self.save()

def expires_before_next_payment_date(self):
if not self.expiry_date:
return False
now = timezone.now()
if now.day > 25:
next_payment_date = (now + relativedelta(months=1)).replace(day=25)
else:
next_payment_date = now.replace(day=25)
return self.expiry_date < next_payment_date

@property
def applied_description(self):
if self.amount_off:
return f"£{self.amount_off:.2f}"
assert self.percent_off
return f"{self.percent_off:.0f}%"
Loading

0 comments on commit 294986a

Please sign in to comment.