Implement handling callbacks

This commit is contained in:
2022-07-21 13:48:39 +01:00
parent 0b6f3ae129
commit 11b5eb50ec
14 changed files with 170 additions and 132 deletions

View File

@@ -2,7 +2,7 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm
from .models import Plan, User
from .models import Plan, Session, User
# Register your models here.
@@ -14,19 +14,12 @@ class CustomUserAdmin(UserAdmin):
*UserAdmin.fieldsets,
(
"Stripe information",
{
"fields": (
"stripe_id",
"subscription_id",
)
},
{"fields": ("stripe_id",)},
),
(
"Payment information",
{
"fields": (
"subscription_active",
"paid",
"plans",
"last_payment",
)
@@ -37,3 +30,4 @@ class CustomUserAdmin(UserAdmin):
admin.site.register(User, CustomUserAdmin)
admin.site.register(Plan)
admin.site.register(Session)

View File

@@ -1,8 +1,10 @@
# Generated by Django 4.0.6 on 2022-07-05 15:55
# Generated by Django 4.0.6 on 2022-07-10 19:54
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
@@ -15,17 +17,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='Plan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('description', models.CharField(blank=True, max_length=1024, null=True)),
('cost', models.IntegerField()),
('product_id', models.UUIDField(blank=True, null=True)),
('image', models.CharField(blank=True, max_length=1024, null=True)),
],
),
migrations.CreateModel(
name='User',
fields=[
@@ -36,18 +27,13 @@ class Migration(migrations.Migration):
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('stripe_id', models.CharField(blank=True, max_length=255, null=True)),
('subscription_id', models.CharField(blank=True, max_length=255, null=True)),
('subscription_active', models.BooleanField(blank=True, null=True)),
('last_payment', models.DateTimeField(blank=True, null=True)),
('paid', models.BooleanField(blank=True, null=True)),
('email', models.EmailField(max_length=254, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('plans', models.ManyToManyField(blank=True, to='core.plan')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
@@ -58,4 +44,35 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Plan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('description', models.CharField(blank=True, max_length=1024, null=True)),
('cost', models.IntegerField()),
('product_id', models.CharField(blank=True, max_length=255, null=True, unique=True)),
('image', models.CharField(blank=True, max_length=1024, null=True)),
],
),
migrations.CreateModel(
name='Session',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('request', models.CharField(blank=True, max_length=255, null=True)),
('subscription_id', models.CharField(blank=True, max_length=255, null=True)),
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.plan')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='user',
name='plans',
field=models.ManyToManyField(blank=True, to='core.plan'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.0.6 on 2022-07-09 09:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='plan',
name='product_id',
field=models.CharField(blank=True, max_length=255, null=True, unique=True),
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(max_length=254, unique=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-10 19:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='session',
name='session',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 4.0.6 on 2022-07-09 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_alter_plan_product_id_alter_user_email'),
]
operations = [
migrations.CreateModel(
name='Session',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254, unique=True)),
('session', models.CharField(max_length=255)),
],
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.0.6 on 2022-07-09 15:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_session'),
]
operations = [
migrations.AlterField(
model_name='session',
name='email',
field=models.EmailField(max_length=254),
),
]

View File

@@ -23,10 +23,7 @@ class Plan(models.Model):
class User(AbstractUser):
# Stripe customer ID
stripe_id = models.CharField(max_length=255, null=True, blank=True)
subscription_id = models.CharField(max_length=255, null=True, blank=True)
subscription_active = models.BooleanField(null=True, blank=True)
last_payment = models.DateTimeField(null=True, blank=True)
paid = models.BooleanField(null=True, blank=True)
plans = models.ManyToManyField(Plan, blank=True)
email = models.EmailField(unique=True)
@@ -60,12 +57,13 @@ class User(AbstractUser):
super().delete(*args, **kwargs)
def has_plan(self, plan):
if not self.paid: # We can't have any plans if we haven't paid
return False
plan_list = [plan.name for plan in self.plans.all()]
return plan in plan_list
class Session(models.Model):
email = models.EmailField()
session = models.CharField(max_length=255)
user = models.ForeignKey(User, on_delete=models.CASCADE)
request = models.CharField(max_length=255, null=True, blank=True)
session = models.CharField(max_length=255, null=True, blank=True)
subscription_id = models.CharField(max_length=255, null=True, blank=True)
plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.CASCADE)

View File

@@ -1,4 +1,6 @@
{% load static %}
{% load has_plan %}
<!DOCTYPE html>
<html lang="en-GB">
<head>
@@ -32,7 +34,7 @@
{% if user.is_authenticated %}
<li><a href="{% url 'billing' %}">Billing</a></li>
{% endif %}
{% if user.paid %}
{% if user|has_plan:'drilldown' %}
<li><a href="{% url 'drilldown' %}">Drilldown</a></li>
{% endif %}
{% if not user.is_authenticated %}

View File

@@ -53,32 +53,5 @@
</div>
</div>
</div>
<script type="text/javascript">
// Create an instance of the Stripe object with your publishable API key
var stripe = Stripe("pk_test_51HbqYzAKLUD9ELc0KSyiQ9YohsfiUCeBpAfpflAIg2Uu2RFecx3sfWYXzM1xDtI5XlQihqHMnaPKd45JzDuqXdGP00pYWvRvRe");
var setupButton = document.getElementById('setup-button');
setupButton.addEventListener("click", function () {
fetch("/setup-bacs", {
method: "POST",
})
.then(function (response) {
return response.json();
})
.then(function (session) {
return stripe.redirectToCheckout({ sessionId: session.id });
})
.then(function (result) {
// If redirectToCheckout fails due to a browser or network
// error, you should display the localized error message to your
// customer using error.message.
if (result.error) {
alert(result.error.message);
}
})
.catch(function (error) {
console.error("Error:", error);
});
});
</script>
{% endblock %}

View File

View File

@@ -0,0 +1,8 @@
from django import template
from core.models import User
register = template.Library()
@register.filter
def has_plan(user, plan_name):
plan_list = [plan.name for plan in user.plans.all()]
return plan_name in plan_list

View File

@@ -1,3 +1,6 @@
import pprint
from datetime import datetime
import stripe
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
@@ -6,13 +9,16 @@ from django.shortcuts import redirect, render
from django.urls import reverse, reverse_lazy
from django.views import View
from django.views.generic.edit import CreateView
from rest_framework.parsers import JSONParser
from rest_framework.views import APIView
from core.forms import NewUserForm
from core.lib.products import assemble_plan_map
from core.models import Plan, Session
from core.models import Plan, Session, User
pp = pprint.PrettyPrinter(indent=4)
# Create your views here
# fmt: off
class Home(View):
@@ -26,8 +32,7 @@ class Billing(LoginRequiredMixin, View):
template_name = "billing.html"
def get(self, request):
context = {"plans": Plan.objects.all(),
"user_plans": request.user.plans.all()}
context = {"plans": Plan.objects.all(), "user_plans": request.user.plans.all()}
return render(request, self.template_name, context)
@@ -37,13 +42,13 @@ class Order(LoginRequiredMixin, View):
try:
session = stripe.checkout.Session.create(
payment_method_types=settings.ALLOWED_PAYMENT_METHODS,
mode='subscription',
mode="subscription",
customer=request.user.stripe_id,
line_items=assemble_plan_map(product_id_filter=product_id),
success_url=request.build_absolute_uri(reverse("success")),
cancel_url=request.build_absolute_uri(reverse("cancel")),
)
Session.objects.create(email=request.user.email, session=session.id)
Session.objects.create(user=request.user, session=session.id)
return redirect(session.url)
# return JsonResponse({'id': session.id})
except Exception as e:
@@ -61,6 +66,84 @@ class Portal(LoginRequiredMixin, View):
def get(self, request):
session = stripe.billing_portal.Session.create(
customer=request.user.stripe_id,
return_url=request.build_absolute_uri(),
return_url=request.build_absolute_uri(reverse("billing")),
)
return redirect(session.url)
class Callback(APIView):
parser_classes = [JSONParser]
def post(self, request):
pp.pprint(request.data)
if request.data is None:
return JsonResponse({"success": False}, status=500)
if "type" in request.data.keys():
rtype = request.data["type"]
if rtype == "checkout.session.completed":
session = request.data["data"]["object"]["id"]
subscription_id = request.data["data"]["object"]["subscription"]
session_map = Session.objects.get(session=session)
print("querying session", session)
if not session_map:
return JsonResponse({"success": False}, status=500)
user = session_map.user
session_map.subscription_id = subscription_id
session_map.save()
if rtype == "customer.subscription.updated":
stripe_id = request.data["data"]["object"]["customer"]
if not stripe_id:
print("No user found for customer:", stripe_id)
return JsonResponse({"success": False}, status=500)
user = User.objects.get(stripe_id=stripe_id)
# ssubscription_active
subscription_id = request.data["data"]["object"]["id"]
sessions = Session.objects.filter(user=user)
session = None
for session_iter in sessions:
if session_iter.subscription_id == subscription_id:
session = session_iter
if not session:
print(f"No session found for subscription id {subscription_id}")
return JsonResponse({"success": False}, status=500)
# query Session objects
# iterate and check against product_id
session.request = request.data["request"]["id"]
product_id = request.data["data"]["object"]["plan"]["id"]
plan = Plan.objects.get(product_id=product_id)
if not plan:
print(f"Plan not found: {product_id}")
return JsonResponse({"success": False}, status=500)
session.plan = plan
session.save()
elif rtype == "payment_intent.succeeded":
customer = request.data["data"]["object"]["customer"]
print("customer", customer)
user = User.objects.get(stripe_id=customer)
if not user:
print("No user found for customer:", customer)
return JsonResponse({"success": False}, status=500)
print("got", user.email)
session = Session.objects.get(request=request.data["request"]["id"])
print("Got session", session)
user.plans.add(session.plan)
print("ADDING PLAN TO USER PLANS")
user.last_payment = datetime.utcnow()
user.save()
elif rtype == "customer.subscription.deleted":
customer = request.data["data"]["object"]["customer"]
user = User.objects.get(stripe_id=customer)
if not user:
print("No user found for customer:", customer)
return JsonResponse({"success": False}, status=500)
product_id = request.data["data"]["object"]["plan"]["id"]
plan = Plan.objects.get(product_id=product_id)
user.plans.remove(plan)
user.save()
else:
return JsonResponse({"success": False}, status=500)
return JsonResponse({"success": True})