From 9a61c237c77e9394083aea8b5cff7cb6deda4d9c Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Wed, 31 Jul 2024 01:02:38 +0200 Subject: [PATCH] WIP: Handle Stripe webhook and add success/cancel pages --- .env.example | 1 + .../templates/accounting/order/cancel.html | 18 ++++ .../templates/accounting/order/detail.html | 2 +- .../templates/accounting/order/success.html | 20 ++++ src/accounting/views.py | 91 ++++++++++++++++++- src/project/settings.py | 2 + 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/accounting/templates/accounting/order/cancel.html create mode 100644 src/accounting/templates/accounting/order/success.html diff --git a/.env.example b/.env.example index 3b6b211..f0d9ade 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,4 @@ DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres # DATABASE_URL=postgres://postgres:postgres@localhost:5432/datacoop_membersystem DEBUG=True STRIPE_API_KEY=sk_test_51Pi1452K58NJXwrP3ayQi0LmGCjiVPUdVFnPOT4didTslRQpmGLSxiuYsmwoTJSLDny5I4uIPcpL2fwpG7GvlTbH00mewgemdz +STRIPE_ENDPOINT_SECRET=whsec_ diff --git a/src/accounting/templates/accounting/order/cancel.html b/src/accounting/templates/accounting/order/cancel.html new file mode 100644 index 0000000..bc73b69 --- /dev/null +++ b/src/accounting/templates/accounting/order/cancel.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block head_title %} + {% trans "Payment cancelled" %} +{% endblock %} + +{% block content %} + +
+

{% trans "Payment canceled" %}

+ +

+ {% trans "Return to order page" %} +

+ +
+{% endblock %} diff --git a/src/accounting/templates/accounting/order/detail.html b/src/accounting/templates/accounting/order/detail.html index cae290b..4fec0ed 100644 --- a/src/accounting/templates/accounting/order/detail.html +++ b/src/accounting/templates/accounting/order/detail.html @@ -41,7 +41,7 @@

{% trans "Total price" %}: {{ order.total_with_vat }}

- {% trans "Pay now" %} + {% trans "Pay now" %}

diff --git a/src/accounting/templates/accounting/order/success.html b/src/accounting/templates/accounting/order/success.html new file mode 100644 index 0000000..c6c04d4 --- /dev/null +++ b/src/accounting/templates/accounting/order/success.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block head_title %} + {% trans "Payment received" %} +{% endblock %} + +{% block content %} + +
+

{% trans "Payment received" %}

+ +

+ {% blocktrans trimmed with order.id as order_id %} + We received your payment for Order {{ order_id }}. + {% endblocktrans %} +

+ +
+{% endblock %} diff --git a/src/accounting/views.py b/src/accounting/views.py index 99ab1c7..bf02c65 100644 --- a/src/accounting/views.py +++ b/src/accounting/views.py @@ -4,10 +4,13 @@ import stripe from django.conf import settings from django.contrib.sites.models import Site from django.core.mail import send_mail +from django.db import transaction from django.http import HttpRequest from django.http import HttpResponse from django.shortcuts import redirect from django.shortcuts import render +from django.urls import reverse +from django.views.decorators.csrf import csrf_exempt from django_view_decorator import namespaced_decorator_factory from . import models @@ -57,7 +60,7 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse: { "price_data": { "currency": item.total_with_vat.currency, - "unit_amount": (item.price, +item.vat).amount, + "unit_amount": int((item.price + item.vat).amount * 100), "product_data": { "name": item.product.name, }, @@ -68,7 +71,8 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse: checkout_session = stripe.checkout.Session.create( line_items=line_items, mode="payment", - success_url=base_domain + "/success", + success_url=base_domain + + reverse("order:success", kwargs={"order_id": order.id, "stripe_session_id": "{CHECKOUT_SESSION_ID}"}), cancel_url=base_domain + "/cancel", ) except Exception as e: @@ -77,3 +81,86 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse: # TODO: Redirect with status=303 return redirect(checkout_session.url) + + +@transaction.atomic +@order_view( + paths="/pay/success//", + name="success", + login_required=True, +) +def success(request: HttpRequest, order_id: int, stripe_session_id: str) -> HttpResponse: + """Create a Stripe session and redirects to Stripe Checkout. + + From Stripe docs: When you have a webhook endpoint set up to listen for checkout.session.completed events and + you set a success_url, Checkout waits for your server to respond to the webhook event delivery before redirecting + your customer. If you use this approach, make sure your server responds to checkout.session.completed events as + quickly as possible. + """ + user = request.user # People just need to login to pay something, not necessarily be a member + order = models.Order.objects.get(pk=order_id, member=user) + + bool(stripe_session_id) + + context = { + "order": order, + } + + return render( + request=request, + template_name="accounting/order/success.html", + context=context, + ) + + +@transaction.atomic +@order_view( + paths="/pay/cancel/", + name="cancel", + login_required=True, +) +def cancel(request: HttpRequest, order_id: int) -> HttpResponse: + """Page to display when a payment is canceled.""" + user = request.user # People just need to login to pay something, not necessarily be a member + order = models.Order.objects.get(pk=order_id, member=user) + + context = { + "order": order, + } + + return render( + request=request, + template_name="accounting/order/cancel.html", + context=context, + ) + + +@csrf_exempt +@order_view( + paths="stripe/webhook/", + name="webhook", + login_required=True, +) +def stripe_webhook(request: HttpRequest) -> HttpResponse: + """Handle Stripe webhook. + + https://docs.stripe.com/metadata/use-cases + """ + payload = request.body + sig_header = request.headers["stripe-signature"] + event = None + + try: + event = stripe.Webhook.construct_event(payload, sig_header, settings.STRIPE_ENDPOINT_SECRET) + except ValueError: + # Invalid payload + return HttpResponse(status=400) + except stripe.error.SignatureVerificationError: + # Invalid signature + return HttpResponse(status=400) + + if event["type"] == "checkout.session.completed" or event["type"] == "checkout.session.async_payment_succeeded": + # TODO: Implement the payment + pass + + return HttpResponse(status=200) diff --git a/src/project/settings.py b/src/project/settings.py index 25e74bf..f88fb56 100644 --- a/src/project/settings.py +++ b/src/project/settings.py @@ -180,6 +180,8 @@ LOGGING = { }, } +STRIPE_API_KEY = env.str("STRIPE_API_KEY") +STRIPE_ENDPOINT_SECRET = env.str("STRIPE_ENDPOINT_SECRET") if DEBUG: INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"]