mirror of
https://github.com/lemeow125/DRF_Template.git
synced 2024-11-17 04:09:25 +08:00
412 lines
14 KiB
Python
412 lines
14 KiB
Python
import json
|
|
import logging
|
|
|
|
import stripe
|
|
from accounts.models import CustomUser
|
|
from config.settings import (
|
|
FRONTEND_ADDRESS,
|
|
FRONTEND_PORT,
|
|
STRIPE_SECRET_KEY,
|
|
STRIPE_SECRET_WEBHOOK,
|
|
URL_SCHEME,
|
|
)
|
|
from django.core.cache import cache
|
|
from django.http import HttpResponse
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from drf_spectacular.utils import extend_schema
|
|
from emails.templates import (
|
|
SubscriptionAvailedEmail,
|
|
SubscriptionCancelledEmail,
|
|
SubscriptionRefundedEmail,
|
|
)
|
|
from payments.serializers import CheckoutSerializer
|
|
from rest_framework import status
|
|
from rest_framework.decorators import api_view
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
from subscriptions.models import SubscriptionPlan, UserSubscription
|
|
from subscriptions.tasks import get_user_subscription
|
|
|
|
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
|
|
if annual:
|
|
PRICE = SUBSCRIPTION.annual_price
|
|
else:
|
|
PRICE = SUBSCRIPTION.monthly_price
|
|
|
|
# Return 404 if no price is set
|
|
if not PRICE:
|
|
return Response(
|
|
{"error": "Specified price does not exist for plan"},
|
|
status=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
|
|
PRICE_ID = PRICE.stripe_price_id
|
|
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 = (
|
|
f"{URL_SCHEME}://{FRONTEND_ADDRESS}:{FRONTEND_PORT}"
|
|
+ "/user/subscription/payment?success=true&agency=False&session_id={CHECKOUT_SESSION_ID}"
|
|
)
|
|
cancel_url = (
|
|
f"{URL_SCHEME}://{FRONTEND_ADDRESS}:{FRONTEND_PORT}"
|
|
+ "/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"])
|
|
# Get the Charge object from the Invoice object
|
|
charge = stripe.Charge.retrieve(invoice["charge"])
|
|
|
|
# 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)
|