-
Rechnungsaddresse von Axa
+ {{ `Rechnungsaddresse von ${userOrganisationName}` }}
diff --git a/server/config/urls.py b/server/config/urls.py
index 32a91864..39f024f8 100644
--- a/server/config/urls.py
+++ b/server/config/urls.py
@@ -10,6 +10,9 @@ from django.views import defaults as default_views
from django.views.decorators.csrf import csrf_exempt
from django_ratelimit.exceptions import Ratelimited
from graphene_django.views import GraphQLView
+from wagtail import urls as wagtail_urls
+from wagtail.admin import urls as wagtailadmin_urls
+from wagtail.documents import urls as wagtaildocs_urls
from vbv_lernwelt.api.directory import list_entities
from vbv_lernwelt.api.user import me_user_view
@@ -55,10 +58,12 @@ from vbv_lernwelt.importer.views import (
t2l_sync,
)
from vbv_lernwelt.notify.views import email_notification_settings
-from vbv_lernwelt.shop.views import get_billing_address, update_billing_address
-from wagtail import urls as wagtail_urls
-from wagtail.admin import urls as wagtailadmin_urls
-from wagtail.documents import urls as wagtaildocs_urls
+from vbv_lernwelt.shop.views import (
+ get_billing_address,
+ update_billing_address,
+ checkout_vv,
+ transaction_webhook,
+)
class SignedIntConverter(IntConverter):
@@ -174,7 +179,9 @@ urlpatterns = [
# shop
path(r'api/shop/billing-address/', get_billing_address, name='get-billing-address'),
- path(r'api/shop/billing-address/update', update_billing_address, name='update-billing-address'),
+ path(r'api/shop/billing-address/update/', update_billing_address, name='update-billing-address'),
+ path(r'api/shop/vv/checkout/', checkout_vv, name='checkout-vv'),
+ path(r'api/shop/transaction/webhook/', django_view_authentication_exempt(transaction_webhook), name='shop-transaction-webhook'),
# importer
path(
diff --git a/server/vbv_lernwelt/shop/views.py b/server/vbv_lernwelt/shop/views.py
index 94755806..1ef6ac05 100644
--- a/server/vbv_lernwelt/shop/views.py
+++ b/server/vbv_lernwelt/shop/views.py
@@ -1,11 +1,41 @@
+import structlog
+from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
-from vbv_lernwelt.shop.models import BillingAddress
+from vbv_lernwelt.shop.models import (
+ BillingAddress,
+ Product,
+ CheckoutInformation,
+ VV_PRODUCT_SKU,
+ CheckoutState,
+)
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
+from vbv_lernwelt.shop.services import DataTransPaymentProvider
+
+logger = structlog.get_logger(__name__)
+
+URL_CHECKOUT_COMPLETE = "/onboarding/vv/checkout/complete"
+URL_CHECKOUT_ADDRESS = "/onboarding/vv/checkout/address"
+
+
+def checkout_error_url(base_url):
+ return f"{base_url}/onboarding/vv/checkout/address?error=true"
+
+
+def checkout_cancel_url(base_url):
+ return f"{base_url}/"
+
+
+def checkout_success_url(base_url):
+ return f"{base_url}/onboarding/vv/checkout/complete"
+
+
+def webhook_url(base_url):
+ return f"{base_url}/api/shop/transaction/webhook"
@api_view(["GET"])
@@ -34,3 +64,125 @@ def update_billing_address(request):
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(["POST"])
+def transaction_webhook(request):
+ """Webhook endpoint for Datatrans to notify about transaction state changes."""
+ logger.info("Transaction Webhook Received", body=request.body)
+
+ datatrans = DataTransPaymentProvider()
+
+ # FIXME verify signature, not working yet
+ # if not datatrans.is_webhook_request_signature_valid(
+ # request=request,
+ # ):
+ # logger.error("Invalid webhook signature")
+ # return JsonResponse({"status": "error"}, status=status.HTTP_400_BAD_REQUEST)
+
+ transaction = request.data
+ transaction_id = transaction["transactionId"]
+ transaction_status = transaction["status"]
+
+ # FIXME just pass the checkout_id as sequence number?
+ checkout_info = CheckoutInformation.objects.get(transaction_id=transaction_id)
+ checkout_info.state = transaction_status
+ checkout_info.webhook_history.append(transaction)
+ checkout_info.save(update_fields=["state", "webhook_history"])
+
+ if transaction_status == ["settled", "transmitted"]:
+ # FIXME create CSU
+ ...
+
+ return JsonResponse({"status": "ok"})
+
+
+@api_view(["POST"])
+@permission_classes([IsAuthenticated])
+def checkout_vv(request):
+ """
+ Checkout for the Versicherungsvermittler product (VV).
+ """
+
+ # The one and only product, thus hardcoded
+ # Expected to be created in the admin interface first.
+ sku = VV_PRODUCT_SKU
+
+ logger.info("Checkout requested", sku=sku, user_id=request.user.id)
+
+ try:
+ product = Product.objects.get(sku=sku)
+ except Product.DoesNotExist:
+ raise Exception(
+ f"Required Product not found: {sku} must be created in the admin interface first.",
+ )
+
+ checkouts = CheckoutInformation.objects.filter(
+ user=request.user,
+ product_sku=sku,
+ )
+
+ datatrans = DataTransPaymentProvider()
+
+ # already purchased (settled or transmitted)
+ if checkouts.filter(
+ state__in=[CheckoutState.SETTLED, CheckoutState.TRANSMITTED]
+ ).exists():
+ return JsonResponse({"next_step_url": URL_CHECKOUT_COMPLETE})
+
+ # FIXME: Re-using seems not to work -> just create a new one for now
+ # Also check if failed transactions get notified via webhook
+ # already initialized -> redirect to payment page again
+ # if checkout := checkouts.filter(state=CheckoutState.INITIALIZED).first():
+ # return JsonResponse(
+ # {
+ # "next_step_url": datatrans.get_payment_url(
+ # transaction_id=checkout.transaction_id
+ # )
+ # }
+ # )
+
+ address = request.data["address"]
+ organization = request.data["organisation_address"]
+ base_redirect_url = request.data["redirect_url"]
+
+ # not yet initialized at all or canceled/failed
+ transaction_id = datatrans.init_transaction(
+ user=request.user,
+ amount_chf_centimes=product.price,
+ redirect_url_success=checkout_success_url(base_redirect_url),
+ redirect_url_error=checkout_error_url(base_redirect_url),
+ redirect_url_cancel=checkout_cancel_url(base_redirect_url),
+ webhook_url=webhook_url(base_redirect_url),
+ )
+
+ # immutable snapshot of data at time of purchase
+ CheckoutInformation.objects.create(
+ user=request.user,
+ state="initialized",
+ transaction_id=transaction_id,
+ # product
+ product_sku=sku,
+ product_price=product.price,
+ product_name=product.name,
+ product_description=product.description,
+ # address
+ first_name=address["firstName"],
+ last_name=address["lastName"],
+ street=address["street"],
+ street_number=address["streetNumber"],
+ postal_code=address["postalCode"],
+ city=address["city"],
+ country=address["country"],
+ # organization
+ company_name=organization.get("name"),
+ company_street=organization.get("street"),
+ company_street_number=organization.get("streetNumber"),
+ company_postal_code=organization.get("postalCode"),
+ company_city=organization.get("city"),
+ company_country=organization.get("country"),
+ )
+
+ return JsonResponse(
+ {"next_step_url": datatrans.get_payment_url(transaction_id=transaction_id)}
+ )