Compare commits

...

2 commits

6 changed files with 182 additions and 10 deletions

View file

@ -6,3 +6,5 @@ DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
# Use something along the the following if you are not using docker # Use something along the the following if you are not using docker
# 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_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

@ -1,21 +1,24 @@
"""Views for the membership app.""" """Views for the membership app."""
from __future__ import annotations import stripe
from django.conf import settings
from typing import TYPE_CHECKING 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.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
if TYPE_CHECKING:
from django.http import HttpRequest
from django.http import HttpResponse
order_view = namespaced_decorator_factory(namespace="order", base_path="order") order_view = namespaced_decorator_factory(namespace="order", base_path="order")
stripe.api_key = settings.STRIPE_API_KEY
@order_view( @order_view(
paths="<int:order_id>/", paths="<int:order_id>/",
@ -36,3 +39,128 @@ def order_detail(request: HttpRequest, order_id: int) -> HttpResponse:
template_name="accounting/order/detail.html", template_name="accounting/order/detail.html",
context=context, context=context,
) )
@order_view(
paths="<int:order_id>/pay/",
name="pay",
login_required=True,
)
def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
"""Create a Stripe session and redirects to Stripe Checkout."""
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)
current_site = Site.objects.get_current(request)
base_domain = f"https://{current_site.domain}"
try:
line_items = []
for item in order.items.all():
line_items.append( # noqa: PERF401
{
"price_data": {
"currency": item.total_with_vat.currency,
"unit_amount": int((item.price + item.vat).amount * 100),
"product_data": {
"name": item.product.name,
},
},
"quantity": item.quantity,
}
)
checkout_session = stripe.checkout.Session.create(
line_items=line_items,
mode="payment",
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:
send_mail("Error in checkout", str(e), settings.DEFAULT_FROM_EMAIL, settings.ADMINS)
raise
# TODO: Redirect with status=303
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

@ -28,6 +28,8 @@ CSRF_TRUSTED_ORIGINS = env.list(
ADMINS = [tuple(x.split(":")) for x in env.list("DJANGO_ADMINS", default=[])] ADMINS = [tuple(x.split(":")) for x in env.list("DJANGO_ADMINS", default=[])]
DEFAULT_FROM_EMAIL = "server@data.coop"
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Application definition # Application definition
@ -178,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"]