Implement handling callbacks

This commit is contained in:
Mark Veidemanis 2022-07-21 13:48:39 +01:00
parent 0b6f3ae129
commit 11b5eb50ec
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
14 changed files with 170 additions and 132 deletions

View File

@ -129,4 +129,10 @@ LOGIN_REDIRECT_URL = "/"
# ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"] # ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"]
ALLOWED_PAYMENT_METHODS = ["card"] ALLOWED_PAYMENT_METHODS = ["card"]
REST_FRAMEWORK = {
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
]
}
from app.local_settings import * # noqa from app.local_settings import * # noqa

View File

@ -20,10 +20,11 @@ from django.urls import include, path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from core.ui.views.drilldown import Drilldown from core.ui.views.drilldown import Drilldown
from core.views import Billing, Home, Order, Portal, Signup from core.views import Billing, Callback, Home, Order, Portal, Signup
urlpatterns = [ urlpatterns = [
path("", Home.as_view(), name="home"), path("", Home.as_view(), name="home"),
path("callback", Callback.as_view(), name="callback"),
path("billing/", Billing.as_view(), name="billing"), path("billing/", Billing.as_view(), name="billing"),
path("order/<str:product_id>/", Order.as_view(), name="order"), path("order/<str:product_id>/", Order.as_view(), name="order"),
path( path(

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm from .forms import CustomUserCreationForm
from .models import Plan, User from .models import Plan, Session, User
# Register your models here. # Register your models here.
@ -14,19 +14,12 @@ class CustomUserAdmin(UserAdmin):
*UserAdmin.fieldsets, *UserAdmin.fieldsets,
( (
"Stripe information", "Stripe information",
{ {"fields": ("stripe_id",)},
"fields": (
"stripe_id",
"subscription_id",
)
},
), ),
( (
"Payment information", "Payment information",
{ {
"fields": ( "fields": (
"subscription_active",
"paid",
"plans", "plans",
"last_payment", "last_payment",
) )
@ -37,3 +30,4 @@ class CustomUserAdmin(UserAdmin):
admin.site.register(User, CustomUserAdmin) admin.site.register(User, CustomUserAdmin)
admin.site.register(Plan) 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.models
import django.contrib.auth.validators import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -15,17 +17,6 @@ class Migration(migrations.Migration):
] ]
operations = [ 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( migrations.CreateModel(
name='User', name='User',
fields=[ 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')), ('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')), ('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')), ('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_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')), ('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')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('stripe_id', models.CharField(blank=True, max_length=255, null=True)), ('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)), ('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')), ('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={ options={
'verbose_name': 'user', 'verbose_name': 'user',
@ -58,4 +44,35 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()), ('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): class User(AbstractUser):
# Stripe customer ID # Stripe customer ID
stripe_id = models.CharField(max_length=255, null=True, blank=True) 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) last_payment = models.DateTimeField(null=True, blank=True)
paid = models.BooleanField(null=True, blank=True)
plans = models.ManyToManyField(Plan, blank=True) plans = models.ManyToManyField(Plan, blank=True)
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
@ -60,12 +57,13 @@ class User(AbstractUser):
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
def has_plan(self, plan): 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()] plan_list = [plan.name for plan in self.plans.all()]
return plan in plan_list return plan in plan_list
class Session(models.Model): class Session(models.Model):
email = models.EmailField() user = models.ForeignKey(User, on_delete=models.CASCADE)
session = models.CharField(max_length=255) 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 static %}
{% load has_plan %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en-GB"> <html lang="en-GB">
<head> <head>
@ -32,7 +34,7 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li><a href="{% url 'billing' %}">Billing</a></li> <li><a href="{% url 'billing' %}">Billing</a></li>
{% endif %} {% endif %}
{% if user.paid %} {% if user|has_plan:'drilldown' %}
<li><a href="{% url 'drilldown' %}">Drilldown</a></li> <li><a href="{% url 'drilldown' %}">Drilldown</a></li>
{% endif %} {% endif %}
{% if not user.is_authenticated %} {% if not user.is_authenticated %}

View File

@ -53,32 +53,5 @@
</div> </div>
</div> </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 %} {% 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 import stripe
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin 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.urls import reverse, reverse_lazy
from django.views import View from django.views import View
from django.views.generic.edit import CreateView 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.forms import NewUserForm
from core.lib.products import assemble_plan_map 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 # Create your views here
# fmt: off
class Home(View): class Home(View):
@ -26,8 +32,7 @@ class Billing(LoginRequiredMixin, View):
template_name = "billing.html" template_name = "billing.html"
def get(self, request): def get(self, request):
context = {"plans": Plan.objects.all(), context = {"plans": Plan.objects.all(), "user_plans": request.user.plans.all()}
"user_plans": request.user.plans.all()}
return render(request, self.template_name, context) return render(request, self.template_name, context)
@ -37,13 +42,13 @@ class Order(LoginRequiredMixin, View):
try: try:
session = stripe.checkout.Session.create( session = stripe.checkout.Session.create(
payment_method_types=settings.ALLOWED_PAYMENT_METHODS, payment_method_types=settings.ALLOWED_PAYMENT_METHODS,
mode='subscription', mode="subscription",
customer=request.user.stripe_id, customer=request.user.stripe_id,
line_items=assemble_plan_map(product_id_filter=product_id), line_items=assemble_plan_map(product_id_filter=product_id),
success_url=request.build_absolute_uri(reverse("success")), success_url=request.build_absolute_uri(reverse("success")),
cancel_url=request.build_absolute_uri(reverse("cancel")), 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 redirect(session.url)
# return JsonResponse({'id': session.id}) # return JsonResponse({'id': session.id})
except Exception as e: except Exception as e:
@ -61,6 +66,84 @@ class Portal(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
session = stripe.billing_portal.Session.create( session = stripe.billing_portal.Session.create(
customer=request.user.stripe_id, customer=request.user.stripe_id,
return_url=request.build_absolute_uri(), return_url=request.build_absolute_uri(reverse("billing")),
) )
return redirect(session.url) 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})