mirror of
https://github.com/lemeow125/DRF_Template.git
synced 2025-06-28 16:15:44 +08:00
Overhauled entire project config, added notifications, email templates, optimized stripe subscriptions, redis caching, and webdriver utilities
This commit is contained in:
parent
7cbe8fd720
commit
99dfcef67b
84 changed files with 4300 additions and 867 deletions
0
backend/payments/__init__.py
Normal file
0
backend/payments/__init__.py
Normal file
6
backend/payments/apps.py
Normal file
6
backend/payments/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PaymentsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'payments'
|
0
backend/payments/migrations/__init__.py
Normal file
0
backend/payments/migrations/__init__.py
Normal file
6
backend/payments/serializers.py
Normal file
6
backend/payments/serializers.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class CheckoutSerializer(serializers.Serializer):
|
||||
subscription_id = serializers.IntegerField()
|
||||
annual = serializers.BooleanField()
|
8
backend/payments/urls.py
Normal file
8
backend/payments/urls.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
from payments import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('checkout_session/', views.StripeCheckoutView.as_view()),
|
||||
path('webhook/', views.stripe_webhook_view, name='Stripe Webhook'),
|
||||
]
|
398
backend/payments/views.py
Normal file
398
backend/payments/views.py
Normal file
|
@ -0,0 +1,398 @@
|
|||
from config.settings import STRIPE_SECRET_KEY, DOMAIN, STRIPE_SECRET_WEBHOOK, CLOUD, BACKEND_URL, FRONTEND_URL
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework import status
|
||||
import logging
|
||||
import stripe
|
||||
from subscriptions.models import SubscriptionPlan, UserSubscription
|
||||
from accounts.models import CustomUser
|
||||
from rest_framework.decorators import api_view
|
||||
from subscriptions.tasks import get_user_subscription
|
||||
import json
|
||||
from emails.templates import SubscriptionAvailedEmail, SubscriptionRefundedEmail, SubscriptionCancelledEmail
|
||||
from django.core.cache import cache
|
||||
from payments.serializers import CheckoutSerializer
|
||||
from drf_spectacular.utils import extend_schema
|
||||
stripe.api_key = STRIPE_SECRET_KEY
|
||||
|
||||
|
||||
@extend_schema(
|
||||
request=CheckoutSerializer
|
||||
)
|
||||
class StripeCheckoutView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# Get subscription ID from POST
|
||||
USER = CustomUser.objects.get(id=self.request.user.id)
|
||||
data = json.loads(request.body)
|
||||
subscription_id = data.get('subscription_id')
|
||||
annual = data.get('annual')
|
||||
|
||||
# Validation for subscription_id field
|
||||
try:
|
||||
subscription_id = int(subscription_id)
|
||||
except:
|
||||
return Response({
|
||||
'error': 'Invalid value specified in subscription_id field'
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Validation for annual field
|
||||
try:
|
||||
annual = bool(annual)
|
||||
except:
|
||||
return Response({
|
||||
'error': 'Invalid value specified in annual field'
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Return an error if the user already has an active subscription
|
||||
EXISTING_SUBSCRIPTION = get_user_subscription(USER.id)
|
||||
if EXISTING_SUBSCRIPTION:
|
||||
return Response({
|
||||
'error': f'User is already subscribed to: {EXISTING_SUBSCRIPTION.subscription.name}'
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Attempt to query the subscription
|
||||
SUBSCRIPTION = SubscriptionPlan.objects.filter(
|
||||
id=subscription_id).first()
|
||||
|
||||
# Return an error if the plan does not exist
|
||||
if not SUBSCRIPTION:
|
||||
return Response({
|
||||
'error': 'Subscription plan not found'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Get the stripe_price_id from the related StripePrice instances
|
||||
PRICE = None
|
||||
PRICE_ID = None
|
||||
|
||||
if annual:
|
||||
PRICE = SUBSCRIPTION.annual_price
|
||||
PRICE_ID = PRICE.stripe_price_id
|
||||
else:
|
||||
PRICE = SUBSCRIPTION.monthly_price
|
||||
PRICE_ID = PRICE.stripe_price_id
|
||||
|
||||
# Return 404 if no price is set
|
||||
if not PRICE or PRICE_ID:
|
||||
return Response({
|
||||
'error': 'Specified price does not exist for plan'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
prorated = PRICE.prorated
|
||||
|
||||
# Return an error if a user is in a user_group and is availing pro-rated plans
|
||||
if not USER.user_group and SUBSCRIPTION.group_exclusive:
|
||||
return Response({
|
||||
'error': 'Regular users cannot avail prorated plans'
|
||||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
success_url = FRONTEND_URL + \
|
||||
'/user/subscription/payment?success=true&agency=False&session_id={CHECKOUT_SESSION_ID}'
|
||||
cancel_url = FRONTEND_URL + '/user/subscription/payment?success=false&user_group=False'
|
||||
|
||||
checkout_session = stripe.checkout.Session.create(
|
||||
line_items=[
|
||||
{
|
||||
'price': PRICE_ID,
|
||||
'quantity': 1
|
||||
} if not prorated else
|
||||
{
|
||||
'price': PRICE_ID,
|
||||
}
|
||||
],
|
||||
mode='subscription',
|
||||
payment_method_types=['card'],
|
||||
success_url=success_url,
|
||||
cancel_url=cancel_url,
|
||||
)
|
||||
return Response({"url": checkout_session.url})
|
||||
except Exception as e:
|
||||
logging.error(str(e))
|
||||
return Response({
|
||||
'error': str(e)
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
@ api_view(['POST'])
|
||||
@ csrf_exempt
|
||||
def stripe_webhook_view(request):
|
||||
payload = request.body
|
||||
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
|
||||
event = None
|
||||
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, sig_header, STRIPE_SECRET_WEBHOOK
|
||||
)
|
||||
except ValueError:
|
||||
# Invalid payload
|
||||
return Response(status=400)
|
||||
except stripe.error.SignatureVerificationError:
|
||||
# Invalid signature
|
||||
return Response(status=401)
|
||||
|
||||
if event['type'] == 'customer.subscription.created':
|
||||
subscription = event['data']['object']
|
||||
# Get the Invoice object from the Subscription object
|
||||
invoice = stripe.Invoice.retrieve(subscription['latest_invoice'])[
|
||||
'data']['object']
|
||||
# Get the Charge object from the Invoice object
|
||||
charge = stripe.Charge.retrieve(invoice['charge'])['data']['object']
|
||||
|
||||
# Get paying user
|
||||
customer = stripe.Customer.retrieve(subscription["customer"])
|
||||
USER = CustomUser.objects.filter(email=customer.email).first()
|
||||
|
||||
product = subscription["items"]["data"][0]
|
||||
SUBSCRIPTION_PLAN = SubscriptionPlan.objects.get(
|
||||
stripe_product_id=product["plan"]["product"])
|
||||
SUBSCRIPTION = UserSubscription.objects.create(
|
||||
subscription=SUBSCRIPTION_PLAN,
|
||||
annual=product["plan"]["interval"] == "year",
|
||||
valid=True,
|
||||
user=USER,
|
||||
stripe_id=subscription['id'])
|
||||
email = SubscriptionAvailedEmail()
|
||||
|
||||
paid = {
|
||||
"amount": charge['amount']/100,
|
||||
"currency": str(charge['currency']).upper()
|
||||
}
|
||||
|
||||
email.context = {
|
||||
"user": USER,
|
||||
"subscription_plan": SUBSCRIPTION_PLAN,
|
||||
"subscription": SUBSCRIPTION,
|
||||
"price_paid": paid,
|
||||
}
|
||||
email.send(to=[customer.email])
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{USER.id}')
|
||||
cache.delete(f'subscriptions_user:{USER.id}')
|
||||
|
||||
# On chargebacks/refunds, invalidate the subscription
|
||||
elif event['type'] == 'charge.refunded':
|
||||
charge = event['data']['object']
|
||||
|
||||
# Get the Invoice object from the Charge object
|
||||
invoice = stripe.Invoice.retrieve(charge['invoice'])
|
||||
|
||||
# Check if the subscription exists
|
||||
SUBSCRIPTION = UserSubscription.objects.filter(
|
||||
stripe_id=invoice['subscription']).first()
|
||||
|
||||
if not (SUBSCRIPTION):
|
||||
return HttpResponse(status=404)
|
||||
|
||||
if SUBSCRIPTION.user:
|
||||
USER = SUBSCRIPTION.user
|
||||
|
||||
# Mark refunded subscription as invalid
|
||||
SUBSCRIPTION.valid = False
|
||||
SUBSCRIPTION.save()
|
||||
|
||||
SUBSCRIPTION_PLAN = SUBSCRIPTION.subscription
|
||||
|
||||
refund = {
|
||||
"amount": charge['amount_refunded']/100,
|
||||
"currency": str(charge['currency']).upper()
|
||||
}
|
||||
|
||||
# Send an email
|
||||
email = SubscriptionRefundedEmail()
|
||||
|
||||
email.context = {
|
||||
"user": USER,
|
||||
"subscription_plan": SUBSCRIPTION_PLAN,
|
||||
"refund": refund
|
||||
}
|
||||
|
||||
email.send(to=[USER.email])
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{USER.id}')
|
||||
|
||||
elif SUBSCRIPTION.user_group:
|
||||
OWNER = SUBSCRIPTION.user_group.owner
|
||||
# Mark refunded subscription as invalid
|
||||
SUBSCRIPTION.valid = False
|
||||
SUBSCRIPTION.save()
|
||||
|
||||
SUBSCRIPTION_PLAN = SUBSCRIPTION.subscription
|
||||
|
||||
refund = {
|
||||
"amount": charge['amount_refunded']/100,
|
||||
"currency": str(charge['currency']).upper()
|
||||
}
|
||||
|
||||
# Send en email
|
||||
email = SubscriptionRefundedEmail()
|
||||
|
||||
email.context = {
|
||||
"user": OWNER,
|
||||
"subscription_plan": SUBSCRIPTION_PLAN,
|
||||
"refund": refund
|
||||
}
|
||||
email.send(to=[OWNER.email])
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{USER.id}')
|
||||
cache.delete(f'subscriptions_user:{USER.id}')
|
||||
|
||||
elif event['type'] == 'customer.subscription.updated':
|
||||
subscription = event['data']['object']
|
||||
|
||||
# Check if the subscription exists
|
||||
SUBSCRIPTION = UserSubscription.objects.filter(
|
||||
stripe_id=subscription['id']).first()
|
||||
|
||||
if not (SUBSCRIPTION):
|
||||
return HttpResponse(status=404)
|
||||
|
||||
# Check if a subscription has been upgraded/downgraded
|
||||
new_stripe_product_id = subscription['items']['data'][0]['plan']['product']
|
||||
current_stripe_product_id = SUBSCRIPTION.subscription.stripe_product_id
|
||||
if new_stripe_product_id != current_stripe_product_id:
|
||||
SUBSCRIPTION_PLAN = SubscriptionPlan.objects.get(
|
||||
stripe_product_id=new_stripe_product_id)
|
||||
SUBSCRIPTION.subscription = SUBSCRIPTION_PLAN
|
||||
SUBSCRIPTION.save()
|
||||
# TODO: Add a plan upgraded email message here
|
||||
|
||||
# Subscription activation/reactivation
|
||||
if subscription['status'] == 'active':
|
||||
SUBSCRIPTION.valid = True
|
||||
SUBSCRIPTION.save()
|
||||
|
||||
if SUBSCRIPTION.user:
|
||||
USER = SUBSCRIPTION.user
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{USER.id}')
|
||||
cache.delete(
|
||||
f'subscriptions_user:{USER.id}')
|
||||
|
||||
elif SUBSCRIPTION.user_group:
|
||||
OWNER = SUBSCRIPTION.user_group.owner
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{OWNER.id}')
|
||||
cache.delete(
|
||||
f'subscriptions_usergroup:{SUBSCRIPTION.user_group.id}')
|
||||
|
||||
# TODO: Add notification here to inform users if their plan has been reactivated
|
||||
|
||||
elif subscription['status'] == 'past_due':
|
||||
# TODO: Add notification here to inform users if their payment method for an existing subscription payment is failing
|
||||
pass
|
||||
|
||||
# If subscriptions get cancelled due to non-payment, invalidate the UserSubscription
|
||||
elif subscription['status'] == 'cancelled':
|
||||
if SUBSCRIPTION.user:
|
||||
USER = SUBSCRIPTION.user
|
||||
|
||||
# Mark refunded subscription as invalid
|
||||
SUBSCRIPTION.valid = False
|
||||
SUBSCRIPTION.save()
|
||||
|
||||
SUBSCRIPTION_PLAN = SUBSCRIPTION.subscription
|
||||
|
||||
# Send an email
|
||||
email = SubscriptionCancelledEmail()
|
||||
|
||||
email.context = {
|
||||
"user": USER,
|
||||
"subscription_plan": SUBSCRIPTION_PLAN,
|
||||
"user_group": False,
|
||||
}
|
||||
email.send(to=[USER.email])
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{USER.id}')
|
||||
cache.delete(f'subscriptions_user:{USER.id}')
|
||||
|
||||
elif SUBSCRIPTION.user_group:
|
||||
OWNER = SUBSCRIPTION.user_group.owner
|
||||
|
||||
# Mark refunded subscription as invalid
|
||||
SUBSCRIPTION.valid = False
|
||||
SUBSCRIPTION.save()
|
||||
|
||||
# Send an email
|
||||
email = SubscriptionCancelledEmail()
|
||||
|
||||
SUBSCRIPTION_PLAN = SUBSCRIPTION.subscription
|
||||
|
||||
email.context = {
|
||||
"user": OWNER,
|
||||
"subscription_plan": SUBSCRIPTION_PLAN
|
||||
}
|
||||
email.send(to=[OWNER.email])
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{OWNER.id}')
|
||||
cache.delete(
|
||||
f'subscriptions_usergroup:{SUBSCRIPTION.user_group.id}')
|
||||
|
||||
# If a subscription gets cancelled, invalidate it
|
||||
elif event['type'] == 'customer.subscription.deleted':
|
||||
subscription = event['data']['object']
|
||||
|
||||
# Check if the subscription exists
|
||||
SUBSCRIPTION = UserSubscription.objects.filter(
|
||||
stripe_id=subscription['id']).first()
|
||||
|
||||
if not (SUBSCRIPTION):
|
||||
return HttpResponse(status=404)
|
||||
|
||||
if SUBSCRIPTION.user:
|
||||
USER = SUBSCRIPTION.user
|
||||
|
||||
# Mark refunded subscription as invalid
|
||||
SUBSCRIPTION.valid = False
|
||||
SUBSCRIPTION.save()
|
||||
|
||||
SUBSCRIPTION_PLAN = SUBSCRIPTION.subscription
|
||||
|
||||
# Send an email
|
||||
email = SubscriptionCancelledEmail()
|
||||
|
||||
email.context = {
|
||||
"user": USER,
|
||||
"subscription_plan": SUBSCRIPTION_PLAN,
|
||||
"user_group": False,
|
||||
}
|
||||
email.send(to=[USER.email])
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{USER.id}')
|
||||
|
||||
elif SUBSCRIPTION.user_group:
|
||||
OWNER = SUBSCRIPTION.user_group.owner
|
||||
|
||||
# Mark refunded subscription as invalid
|
||||
SUBSCRIPTION.valid = False
|
||||
SUBSCRIPTION.save()
|
||||
|
||||
# Send an email
|
||||
email = SubscriptionCancelledEmail()
|
||||
|
||||
SUBSCRIPTION_PLAN = SUBSCRIPTION.subscription
|
||||
|
||||
email.context = {
|
||||
"user": OWNER,
|
||||
"subscription_plan": SUBSCRIPTION_PLAN
|
||||
}
|
||||
email.send(to=[OWNER.email])
|
||||
|
||||
# Clear cache
|
||||
cache.delete(f'billing_user:{OWNER.id}')
|
||||
|
||||
# Passed signature verification
|
||||
return HttpResponse(status=200)
|
Loading…
Add table
Add a link
Reference in a new issue