membersystem/src/accounting/views.py
Benjamin Bach 1070e93885
All checks were successful
continuous-integration/drone/push Build is passing
Divide the amount received from Stripe by 100.0 (#40)
Reviewed-on: #40
Reviewed-by: valberg <valberg@orn.li>
Co-authored-by: Benjamin Bach <benjamin@overtag.dk>
Co-committed-by: Benjamin Bach <benjamin@overtag.dk>
2024-08-04 07:34:50 +00:00

178 lines
5.8 KiB
Python

"""Views for the membership app."""
import stripe
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.mail import mail_admins
from django.db import transaction
from django.http import HttpRequest
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
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 djmoney.money import Money
from . import models
order_view = namespaced_decorator_factory(namespace="order", base_path="order")
stripe.api_key = settings.STRIPE_API_KEY
@order_view(
paths="<int:order_id>/",
name="detail",
login_required=True,
)
def order_detail(request: HttpRequest, order_id: int) -> HttpResponse:
"""View to show the details of a member."""
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/detail.html",
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}"
if settings.DEBUG:
f"http://{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,
metadata={"order_id": order.id},
mode="payment",
success_url=base_domain + reverse("order:success", kwargs={"order_id": order.id}),
cancel_url=base_domain + "/cancel",
)
except Exception as e:
mail_admins("Error in checkout", str(e))
raise
# TODO: Redirect with status=303
return redirect(checkout_session.url)
@transaction.atomic
@order_view(
paths="<int:order_id>/pay/success/",
name="success",
login_required=True,
)
def success(request: HttpRequest, order_id: int) -> 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)
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,
)
@transaction.atomic
@order_view(
paths="stripe/webhook/",
name="webhook",
)
@csrf_exempt
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":
# Order is marked paid via signals, Membership is activated via signals.
order_id = event["data"]["object"]["metadata"]["order_id"]
order = get_object_or_404(models.Order, pk=order_id)
if not models.Payment.objects.filter(order=order).exists():
models.Payment.objects.create(
order=order,
amount=Money(event["data"]["object"]["amount_total"] / 100.0, event["data"]["object"]["currency"]),
description="Paid via Stripe",
payment_type=models.PaymentType.objects.get_or_create(name="Stripe")[0],
external_transaction_id=event["id"],
)
return HttpResponse(status=200)