WIP: Handle Stripe webhook and add success/cancel pages
Some checks failed
continuous-integration/drone/pr Build is failing

This commit is contained in:
Benjamin Bach 2024-07-31 01:02:38 +02:00
parent 1029162a62
commit 9a61c237c7
No known key found for this signature in database
GPG key ID: 486F0D69C845416E
6 changed files with 131 additions and 3 deletions

View file

@ -7,3 +7,4 @@ DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
# DATABASE_URL=postgres://postgres:postgres@localhost:5432/datacoop_membersystem # DATABASE_URL=postgres://postgres:postgres@localhost:5432/datacoop_membersystem
DEBUG=True DEBUG=True
STRIPE_API_KEY=sk_test_51Pi1452K58NJXwrP3ayQi0LmGCjiVPUdVFnPOT4didTslRQpmGLSxiuYsmwoTJSLDny5I4uIPcpL2fwpG7GvlTbH00mewgemdz STRIPE_API_KEY=sk_test_51Pi1452K58NJXwrP3ayQi0LmGCjiVPUdVFnPOT4didTslRQpmGLSxiuYsmwoTJSLDny5I4uIPcpL2fwpG7GvlTbH00mewgemdz
STRIPE_ENDPOINT_SECRET=whsec_

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% load i18n %}
{% block head_title %}
{% trans "Payment cancelled" %}
{% endblock %}
{% block content %}
<div class="content-view">
<h2>{% trans "Payment canceled" %}</h2>
<p>
<a href="{% order:detail order_id=order.id %}">{% trans "Return to order page" %}</a>
</p>
</div>
{% endblock %}

View file

@ -41,7 +41,7 @@
<h2>{% trans "Total price" %}: {{ order.total_with_vat }}</h2> <h2>{% trans "Total price" %}: {{ order.total_with_vat }}</h2>
<p> <p>
<a href="" class="button">{% trans "Pay now" %}</a> <a href="{% url "order:pay" order_id=order.pk %}" class="button">{% trans "Pay now" %}</a>
</p> </p>
</div> </div>

View file

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% load i18n %}
{% block head_title %}
{% trans "Payment received" %}
{% endblock %}
{% block content %}
<div class="content-view">
<h2>{% trans "Payment received" %}</h2>
<p>
{% blocktrans trimmed with order.id as order_id %}
We received your payment for Order {{ order_id }}.
{% endblocktrans %}
</p>
</div>
{% endblock %}

View file

@ -4,10 +4,13 @@ import stripe
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db import transaction
from django.http import HttpRequest from django.http import HttpRequest
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.shortcuts import render 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 django_view_decorator import namespaced_decorator_factory
from . import models from . import models
@ -57,7 +60,7 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
{ {
"price_data": { "price_data": {
"currency": item.total_with_vat.currency, "currency": item.total_with_vat.currency,
"unit_amount": (item.price, +item.vat).amount, "unit_amount": int((item.price + item.vat).amount * 100),
"product_data": { "product_data": {
"name": item.product.name, "name": item.product.name,
}, },
@ -68,7 +71,8 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
checkout_session = stripe.checkout.Session.create( checkout_session = stripe.checkout.Session.create(
line_items=line_items, line_items=line_items,
mode="payment", 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", cancel_url=base_domain + "/cancel",
) )
except Exception as e: except Exception as e:
@ -77,3 +81,86 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
# TODO: Redirect with status=303 # TODO: Redirect with status=303
return redirect(checkout_session.url) return redirect(checkout_session.url)
@transaction.atomic
@order_view(
paths="<int:order_id>/pay/success/<str:stripe_session_id>/",
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="<int:order_id>/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)

View file

@ -180,6 +180,8 @@ LOGGING = {
}, },
} }
STRIPE_API_KEY = env.str("STRIPE_API_KEY")
STRIPE_ENDPOINT_SECRET = env.str("STRIPE_ENDPOINT_SECRET")
if DEBUG: if DEBUG:
INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"] INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"]