From 11b5eb50ecd65f72dc9dd77353442b4968b1862f Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Thu, 21 Jul 2022 13:48:39 +0100 Subject: [PATCH] Implement handling callbacks --- app/settings.py | 6 ++ app/urls.py | 3 +- core/admin.py | 12 +-- core/migrations/0001_initial.py | 53 ++++++---- ..._alter_plan_product_id_alter_user_email.py | 23 ----- core/migrations/0002_session_session.py | 18 ++++ core/migrations/0003_session.py | 21 ---- core/migrations/0004_alter_session_email.py | 18 ---- core/models.py | 12 +-- core/templates/base.html | 4 +- core/templates/billing.html | 27 ------ core/templatetags/__init__.py | 0 core/templatetags/has_plan.py | 8 ++ core/views.py | 97 +++++++++++++++++-- 14 files changed, 170 insertions(+), 132 deletions(-) delete mode 100644 core/migrations/0002_alter_plan_product_id_alter_user_email.py create mode 100644 core/migrations/0002_session_session.py delete mode 100644 core/migrations/0003_session.py delete mode 100644 core/migrations/0004_alter_session_email.py create mode 100644 core/templatetags/__init__.py create mode 100644 core/templatetags/has_plan.py diff --git a/app/settings.py b/app/settings.py index 6d937d9..6f64903 100644 --- a/app/settings.py +++ b/app/settings.py @@ -129,4 +129,10 @@ LOGIN_REDIRECT_URL = "/" # ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"] ALLOWED_PAYMENT_METHODS = ["card"] +REST_FRAMEWORK = { + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", + ] +} + from app.local_settings import * # noqa diff --git a/app/urls.py b/app/urls.py index 405d03c..c27a94c 100644 --- a/app/urls.py +++ b/app/urls.py @@ -20,10 +20,11 @@ from django.urls import include, path from django.views.generic import TemplateView 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 = [ path("", Home.as_view(), name="home"), + path("callback", Callback.as_view(), name="callback"), path("billing/", Billing.as_view(), name="billing"), path("order//", Order.as_view(), name="order"), path( diff --git a/core/admin.py b/core/admin.py index efc893c..0fdd381 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 957f785..26a2c7b 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -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'), + ), ] diff --git a/core/migrations/0002_alter_plan_product_id_alter_user_email.py b/core/migrations/0002_alter_plan_product_id_alter_user_email.py deleted file mode 100644 index 4459c4a..0000000 --- a/core/migrations/0002_alter_plan_product_id_alter_user_email.py +++ /dev/null @@ -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), - ), - ] diff --git a/core/migrations/0002_session_session.py b/core/migrations/0002_session_session.py new file mode 100644 index 0000000..7451043 --- /dev/null +++ b/core/migrations/0002_session_session.py @@ -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), + ), + ] diff --git a/core/migrations/0003_session.py b/core/migrations/0003_session.py deleted file mode 100644 index 1de8f6b..0000000 --- a/core/migrations/0003_session.py +++ /dev/null @@ -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)), - ], - ), - ] diff --git a/core/migrations/0004_alter_session_email.py b/core/migrations/0004_alter_session_email.py deleted file mode 100644 index fb8f001..0000000 --- a/core/migrations/0004_alter_session_email.py +++ /dev/null @@ -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), - ), - ] diff --git a/core/models.py b/core/models.py index 6b7e149..99a51a8 100644 --- a/core/models.py +++ b/core/models.py @@ -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) diff --git a/core/templates/base.html b/core/templates/base.html index bb3499e..816daad 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,4 +1,6 @@ {% load static %} +{% load has_plan %} + @@ -32,7 +34,7 @@ {% if user.is_authenticated %}
  • Billing
  • {% endif %} - {% if user.paid %} + {% if user|has_plan:'drilldown' %}
  • Drilldown
  • {% endif %} {% if not user.is_authenticated %} diff --git a/core/templates/billing.html b/core/templates/billing.html index 490bdbc..2742381 100644 --- a/core/templates/billing.html +++ b/core/templates/billing.html @@ -53,32 +53,5 @@ - {% endblock %} diff --git a/core/templatetags/__init__.py b/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/templatetags/has_plan.py b/core/templatetags/has_plan.py new file mode 100644 index 0000000..411327c --- /dev/null +++ b/core/templatetags/has_plan.py @@ -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 \ No newline at end of file diff --git a/core/views.py b/core/views.py index 9c31487..e4d3d83 100644 --- a/core/views.py +++ b/core/views.py @@ -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})