213 lines
6.6 KiB
Python
213 lines
6.6 KiB
Python
import structlog
|
|
from django.http import JsonResponse
|
|
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.course.models import CourseSession, CourseSessionUser
|
|
from vbv_lernwelt.shop.models import (
|
|
BillingAddress,
|
|
CheckoutInformation,
|
|
CheckoutState,
|
|
Product,
|
|
VV_PRODUCT_SKU,
|
|
)
|
|
from vbv_lernwelt.shop.serializers import BillingAddressSerializer
|
|
from vbv_lernwelt.shop.services import (
|
|
datatrans_state_to_checkout_state,
|
|
get_payment_url,
|
|
init_transaction,
|
|
InitTransactionException,
|
|
is_signature_valid,
|
|
)
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
COURSE_SESSION_TITLE_VV = "Versicherungsvermittler/-in"
|
|
|
|
|
|
@api_view(["GET"])
|
|
@permission_classes([IsAuthenticated])
|
|
def get_billing_address(request):
|
|
try:
|
|
billing_address = BillingAddress.objects.get(user=request.user)
|
|
data = BillingAddressSerializer(billing_address).data
|
|
except BillingAddress.DoesNotExist:
|
|
data = BillingAddressSerializer().data
|
|
data["first_name"] = request.user.first_name # noqa
|
|
data["last_name"] = request.user.last_name # noqa
|
|
|
|
return Response(data)
|
|
|
|
|
|
@api_view(["PUT"])
|
|
@permission_classes([IsAuthenticated])
|
|
def update_billing_address(request):
|
|
try:
|
|
billing_address = BillingAddress.objects.get(user=request.user)
|
|
except BillingAddress.DoesNotExist:
|
|
billing_address = None
|
|
|
|
serializer = BillingAddressSerializer(
|
|
billing_address, data=request.data, partial=True
|
|
)
|
|
|
|
if serializer.is_valid():
|
|
serializer.save(user=request.user)
|
|
return Response(serializer.data)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
@api_view(["POST"])
|
|
def transaction_webhook(request):
|
|
"""
|
|
VERY IMPORTANT: Datatrans had/has to set up stuff manually on their side:
|
|
Otherwise, this webhook was/will not be called for "initialized" -> "failed" state changes
|
|
For timed out transactions (cleaned up in 15 minute intervals, after 30 minutes by them),
|
|
"""
|
|
logger.info("Webhook: Datatrans called transaction webhook", body=request.body)
|
|
|
|
if not is_signature_valid(
|
|
signature=request.headers.get("Datatrans-Signature", ""),
|
|
payload=request.body,
|
|
):
|
|
logger.warning("Invalid signature")
|
|
return JsonResponse({"status": "invalid signature"}, status=400)
|
|
|
|
transaction = request.data
|
|
transaction_id = transaction["transactionId"]
|
|
|
|
# keep webhook history (for debugging)
|
|
checkout_info = CheckoutInformation.objects.get(transaction_id=transaction_id)
|
|
checkout_info.webhook_history.append(transaction)
|
|
checkout_info.save(update_fields=["webhook_history"])
|
|
|
|
# update checkout state
|
|
checkout_state = datatrans_state_to_checkout_state(transaction["status"])
|
|
update_checkout_state(checkout_info=checkout_info, state=checkout_state)
|
|
|
|
# handle paid
|
|
if checkout_state == CheckoutState.PAID:
|
|
create_vv_course_session_user(checkout_info=checkout_info)
|
|
|
|
return JsonResponse({"status": "ok"})
|
|
|
|
|
|
@api_view(["POST"])
|
|
@permission_classes([IsAuthenticated])
|
|
def checkout_vv(request):
|
|
"""
|
|
Checkout for the Versicherungsvermittler product (VV).
|
|
|
|
VV_PRODUCT_SKU: The one and only product, thus hardcoded
|
|
Expected to be created in the admin interface first.
|
|
"""
|
|
sku = VV_PRODUCT_SKU
|
|
logger.info(f"Checkout requested: sku={sku}", user_id=request.user.id)
|
|
base_redirect_url = request.data["redirect_url"]
|
|
|
|
try:
|
|
product = Product.objects.get(sku=sku)
|
|
except Product.DoesNotExist:
|
|
return next_step_response(
|
|
url=checkout_error_url(
|
|
base_url=base_redirect_url,
|
|
message="vv_product_does_not_exist_needs_to_be_created",
|
|
),
|
|
)
|
|
|
|
checkouts = CheckoutInformation.objects.filter(
|
|
user=request.user,
|
|
product_sku=sku,
|
|
)
|
|
|
|
# already paid (successfully)-> redirect to home
|
|
if checkouts.filter(state__in=[CheckoutState.PAID]).exists():
|
|
return next_step_response(url="/")
|
|
|
|
# already initialized -> redirect to payment page again
|
|
if checkout := checkouts.filter(state=CheckoutState.INITIALIZED).first():
|
|
return next_step_response(url=get_payment_url(checkout.transaction_id))
|
|
|
|
# not yet initialized at all, or canceled/failed
|
|
# -> create new transaction and checkout
|
|
try:
|
|
transaction_id = 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),
|
|
)
|
|
except InitTransactionException as e:
|
|
return next_step_response(
|
|
url=checkout_error_url(
|
|
base_url=base_redirect_url,
|
|
),
|
|
)
|
|
|
|
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
|
|
**request.data["address"],
|
|
)
|
|
|
|
return next_step_response(url=get_payment_url(transaction_id))
|
|
|
|
|
|
def update_checkout_state(checkout_info: CheckoutInformation, state: CheckoutState):
|
|
checkout_info.state = state.value
|
|
checkout_info.save(update_fields=["state"])
|
|
|
|
|
|
def create_vv_course_session_user(checkout_info: CheckoutInformation):
|
|
logger.info("Creating VV course session user", user_id=checkout_info.user_id)
|
|
CourseSessionUser.objects.get_or_create(
|
|
user=checkout_info.user,
|
|
role=CourseSessionUser.Role.MEMBER,
|
|
course_session=CourseSession.objects.filter(
|
|
title=COURSE_SESSION_TITLE_VV
|
|
).first(),
|
|
)
|
|
|
|
|
|
def next_step_response(
|
|
url: str,
|
|
) -> JsonResponse:
|
|
return JsonResponse(
|
|
{
|
|
"next_step_url": url,
|
|
},
|
|
)
|
|
|
|
|
|
def webhook_url(base_url: str) -> str:
|
|
return f"{base_url}/api/shop/transaction/webhook/"
|
|
|
|
|
|
def checkout_error_url(base_url: str, message: str | None = None) -> str:
|
|
url = f"{base_url}/onboarding/vv/checkout/address?error"
|
|
|
|
if message:
|
|
url += f"&message={message}"
|
|
|
|
return url
|
|
|
|
|
|
def checkout_cancel_url(base_url: str) -> str:
|
|
return f"{base_url}/"
|
|
|
|
|
|
def checkout_success_url(base_url: str = "") -> str:
|
|
return f"{base_url}/onboarding/vv/checkout/complete"
|