Overhauled entire project config, added notifications, email templates, optimized stripe subscriptions, redis caching, and webdriver utilities

This commit is contained in:
Keannu Christian Bernasol 2024-05-10 23:15:29 +08:00
parent 7cbe8fd720
commit 99dfcef67b
84 changed files with 4300 additions and 867 deletions

View file

View file

@ -0,0 +1,29 @@
from django.contrib import admin
from subscriptions.models import StripePrice, SubscriptionPlan, UserSubscription
from unfold.admin import ModelAdmin
from unfold.contrib.filters.admin import RangeDateFilter
@admin.register(StripePrice)
class StripePriceAdmin(ModelAdmin):
search_fields = ["id", "lookup_key",
"stripe_price_id","price","currency", "prorated", "annual"]
list_display = ["id", "lookup_key",
"stripe_price_id", "price", "currency", "prorated", "annual"]
@admin.register(SubscriptionPlan)
class SubscriptionPlanAdmin(ModelAdmin):
list_display = ["id", "__str__", "stripe_product_id", "group_exclusive"]
search_fields = ["id", "name", "stripe_product_id", "group_exclusive"]
@admin.register(UserSubscription)
class UserSubscriptionAdmin(ModelAdmin):
list_filter_submit = True
list_filter = ((
"date", RangeDateFilter
),)
list_display = ["id", "__str__", "valid", "annual",
"date"]
search_fields = ["id", "date"]

View file

@ -0,0 +1,9 @@
from django.apps import AppConfig
class SubscriptionConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'subscriptions'
def ready(self):
import subscriptions.signals

View file

@ -0,0 +1,56 @@
# Generated by Django 5.0.6 on 2024-05-10 06:37
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('user_groups', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='StripePrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('annual', models.BooleanField(default=False)),
('stripe_price_id', models.CharField(max_length=100)),
('price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)),
('currency', models.CharField(max_length=20)),
('lookup_key', models.CharField(blank=True, max_length=100, null=True)),
('prorated', models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name='SubscriptionPlan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(max_length=1024, null=True)),
('stripe_product_id', models.CharField(max_length=100)),
('group_exclusive', models.BooleanField(default=False)),
('annual_price', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='annual_plan', to='subscriptions.stripeprice')),
('monthly_price', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='monthly_plan', to='subscriptions.stripeprice')),
],
),
migrations.CreateModel(
name='UserSubscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stripe_id', models.CharField(max_length=100)),
('date', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
('valid', models.BooleanField()),
('annual', models.BooleanField()),
('subscription', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='subscriptions.subscriptionplan')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('user_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='user_groups.usergroup')),
],
),
]

View file

@ -0,0 +1,56 @@
from django.db import models
from accounts.models import CustomUser
from user_groups.models import UserGroup
from django.utils.timezone import now
class StripePrice(models.Model):
annual = models.BooleanField(default=False)
stripe_price_id = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
currency = models.CharField(max_length=20)
lookup_key = models.CharField(max_length=100, blank=True, null=True)
prorated = models.BooleanField(default=False)
def __str__(self):
if self.annual:
return f"{self.price}{self.currency}/year"
else:
return f"{self.price}{self.currency}/month"
class SubscriptionPlan(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(max_length=1024, null=True)
stripe_product_id = models.CharField(max_length=100)
annual_price = models.ForeignKey(
StripePrice, on_delete=models.SET_NULL, related_name='annual_plan', null=True)
monthly_price = models.ForeignKey(
StripePrice, on_delete=models.SET_NULL, related_name='monthly_plan', null=True)
group_exclusive = models.BooleanField(default=False)
def __str__(self):
return f"{self.name}"
# Model for User Subscriptions
class UserSubscription(models.Model):
user = models.ForeignKey(
CustomUser, on_delete=models.CASCADE, blank=True, null=True)
user_group = models.ForeignKey(
UserGroup, on_delete=models.CASCADE, blank=True, null=True)
subscription = models.ForeignKey(
SubscriptionPlan, on_delete=models.SET_NULL, blank=True, null=True)
stripe_id = models.CharField(max_length=100)
date = models.DateTimeField(default=now, editable=False)
valid = models.BooleanField()
annual = models.BooleanField()
def __str__(self):
if self.user:
return f'Subscription {self.subscription.name} for {self.user}'
else:
return f'Subscription {self.subscription.name} for {self.user_group}'

View file

@ -0,0 +1,44 @@
from rest_framework import serializers
from subscriptions.models import SubscriptionPlan, UserSubscription, StripePrice
from accounts.serializers import SimpleCustomUserSerializer
class SimpleStripePriceSerializer(serializers.ModelSerializer):
class Meta:
model = StripePrice
fields = ['price', 'currency', 'prorated']
class SubscriptionPlanSerializer(serializers.ModelSerializer):
class Meta:
model = SubscriptionPlan
fields = ['id', 'name', 'description',
'annual_price', 'monthly_price', 'group_exclusive']
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['annual_price'] = SimpleStripePriceSerializer(
instance.annual_price, many=False).data
representation['monthly_price'] = SimpleStripePriceSerializer(
instance.monthly_price, many=False).data
return representation
class UserSubscriptionSerializer(serializers.ModelSerializer):
date = serializers.DateTimeField(
format="%m-%d-%Y %I:%M %p", read_only=True)
class Meta:
model = UserSubscription
fields = ['id', 'user', 'user_group', 'subscription',
'date', 'valid', 'annual']
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['user'] = SimpleCustomUserSerializer(
instance.user, many=False).data
representation['subscription'] = SubscriptionPlanSerializer(
instance.subscription, many=False).data
return representation

View file

@ -0,0 +1,91 @@
from django.dispatch import receiver
from django.db.models.signals import post_migrate, post_save
from .models import UserSubscription, StripePrice, SubscriptionPlan
from django.core.cache import cache
from config.settings import STRIPE_SECRET_KEY
import stripe
stripe.api_key = STRIPE_SECRET_KEY
# Template for running actions after user have paid for a subscription
@receiver(post_save, sender=SubscriptionPlan)
def clear_cache_after_plan_updates(sender, instance, **kwargs):
# Clear cache
cache.delete('subscriptionplans')
@receiver(post_save, sender=UserSubscription)
def scan_after_payment(sender, instance, **kwargs):
# If the updated/created subscription is valid
if instance.valid and instance.user:
# TODO: Add any Celery task actions here for regular subscription payees
pass
@receiver(post_migrate)
def create_subscriptions(sender, **kwargs):
if sender.name == 'subscriptions':
print('Importing data from Stripe')
created_prices = 0
created_plans = 0
skipped_prices = 0
skipped_plans = 0
products = stripe.Product.list(active=True)
prices = stripe.Price.list(expand=["data.tiers"], active=True)
# Create the StripePrice
for price in prices['data']:
annual = (price['recurring']['interval'] ==
'year') if price['recurring'] else False
STRIPE_PRICE, CREATED = StripePrice.objects.get_or_create(
stripe_price_id=price['id'],
price=price['unit_amount'] / 100,
annual=annual,
lookup_key=price['lookup_key'],
prorated=price['recurring']['usage_type'] == 'metered',
currency=price['currency']
)
if CREATED:
created_prices += 1
else:
skipped_prices += 1
# Create the SubscriptionPlan
for product in products['data']:
ANNUAL_PRICE = None
MONTHLY_PRICE = None
for price in prices['data']:
if price['product'] == product['id']:
STRIPE_PRICE = StripePrice.objects.get(
stripe_price_id=price['id'],
)
if STRIPE_PRICE.annual:
ANNUAL_PRICE = STRIPE_PRICE
else:
MONTHLY_PRICE = STRIPE_PRICE
if ANNUAL_PRICE or MONTHLY_PRICE:
SUBSCRIPTION_PLAN, CREATED = SubscriptionPlan.objects.get_or_create(
name=product['name'],
description=product['description'],
stripe_product_id=product['id'],
annual_price=ANNUAL_PRICE,
monthly_price=MONTHLY_PRICE,
group_exclusive=product['metadata']['group_exclusive'] == 'True'
)
if CREATED:
created_plans += 1
else:
skipped_plans += 1
# Skip over plans with missing pricing rates
else:
print('Skipping plan' +
product['name'] + 'with missing pricing data')
# Assign the StripePrice to the SubscriptionPlan
SUBSCRIPTION_PLAN.save()
print('Created', created_plans, 'new plans')
print('Skipped', skipped_plans, 'existing plans')
print('Created', created_prices, 'new prices')
print('Skipped', skipped_prices, 'existing prices')

View file

@ -0,0 +1,42 @@
from celery import shared_task
@shared_task
def get_user_subscription(user_id):
from subscriptions.models import UserSubscription
from accounts.models import CustomUser
USER = CustomUser.objects.get(id=user_id)
# Get a list of subscriptions for the specified user
active_subscriptions = None
if USER.user_group:
active_subscriptions = UserSubscription.objects.filter(
user_group=USER.user_group, valid=True)
else:
active_subscriptions = UserSubscription.objects.filter(
user=USER, valid=True)
# Return first valid subscription if there is one
if len(active_subscriptions) > 0:
return active_subscriptions[0]
else:
return None
@shared_task
def get_user_group_subscription(user_group):
from subscriptions.models import UserSubscription
from user_groups.models import UserGroup
USER_GROUP = UserGroup.objects.get(id=user_group)
# Get a list of subscriptions for the specified user
active_subscriptions = None
active_subscriptions = UserSubscription.objects.filter(
user_group=USER_GROUP, valid=True)
# Return first valid subscription if there is one
if len(active_subscriptions) > 0:
return active_subscriptions[0]
else:
return None

View file

@ -0,0 +1,14 @@
from django.urls import path, include
from subscriptions import views
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'plans', views.SubscriptionPlanViewset,
basename="Subscription Plans")
router.register(r'self', views.UserSubscriptionViewset,
basename="Self Subscriptions")
router.register(r'user_group', views.UserGroupSubscriptionViewet,
basename="Group Subscriptions")
urlpatterns = [
path('', include(router.urls)),
]

View file

@ -0,0 +1,56 @@
from subscriptions.serializers import SubscriptionPlanSerializer, UserSubscriptionSerializer
from subscriptions.models import SubscriptionPlan, UserSubscription
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework import viewsets
from django.core.cache import cache
class SubscriptionPlanViewset(viewsets.ModelViewSet):
http_method_names = ['get']
serializer_class = SubscriptionPlanSerializer
permission_classes = [AllowAny]
queryset = SubscriptionPlan.objects.all()
def get_queryset(self):
key = 'subscriptionplans'
queryset = cache.get(key)
if not queryset:
queryset = super().get_queryset()
cache.set(key, queryset, 60*60)
return queryset
class UserSubscriptionViewset(viewsets.ModelViewSet):
http_method_names = ['get']
serializer_class = UserSubscriptionSerializer
permission_classes = [IsAuthenticated]
queryset = UserSubscription.objects.all()
def get_queryset(self):
user = self.request.user
key = f'subscriptions_user:{user.id}'
queryset = cache.get(key)
if not queryset:
queryset = UserSubscription.objects.filter(user=user)
cache.set(key, queryset, 60*60)
return queryset
class UserGroupSubscriptionViewet(viewsets.ModelViewSet):
http_method_names = ['get']
serializer_class = UserSubscriptionSerializer
permission_classes = [IsAuthenticated]
queryset = UserSubscription.objects.all()
def get_queryset(self):
user = self.request.user
if not user.user_group:
return UserSubscription.objects.none()
else:
key = f'subscriptions_usergroup:{user.user_group.id}'
queryset = cache.get(key)
if not cache:
queryset = UserSubscription.objects.filter(
user_group=user.user_group)
cache.set(key, queryset, 60*60)
return queryset